summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/alloc.c52
-rw-r--r--tests/bfs/D_all.out (renamed from tests/test_S_dfs.out)0
-rw-r--r--tests/bfs/D_all.sh1
-rw-r--r--tests/bfs/D_incomplete.sh1
-rw-r--r--tests/bfs/D_multi.out (renamed from tests/test_fprint.out)0
-rw-r--r--tests/bfs/D_multi.sh1
-rw-r--r--tests/bfs/D_opt.out (renamed from tests/test_type_f.out)0
-rw-r--r--tests/bfs/D_opt.sh1
-rw-r--r--tests/bfs/D_unknown.out (renamed from tests/test_O0.out)12
-rw-r--r--tests/bfs/D_unknown.sh4
-rw-r--r--tests/bfs/L_capable.out2
-rw-r--r--tests/bfs/L_capable.sh10
-rw-r--r--tests/bfs/L_unique.out (renamed from tests/test_L_unique.out)0
-rw-r--r--tests/bfs/L_unique.sh1
-rw-r--r--tests/bfs/L_unique_depth.out (renamed from tests/test_L_unique_depth.out)0
-rw-r--r--tests/bfs/L_unique_depth.sh1
-rw-r--r--tests/bfs/L_unique_loops.out (renamed from tests/test_L_unique_loops.out)0
-rw-r--r--tests/bfs/L_unique_loops.sh1
-rw-r--r--tests/bfs/O0.out (renamed from tests/test_O1.out)12
-rw-r--r--tests/bfs/O0.sh1
-rw-r--r--tests/bfs/O1.out (renamed from tests/test_O2.out)12
-rw-r--r--tests/bfs/O1.sh1
-rw-r--r--tests/bfs/O2.out (renamed from tests/test_O3.out)12
-rw-r--r--tests/bfs/O2.sh1
-rw-r--r--tests/bfs/O3.out19
-rw-r--r--tests/bfs/O3.sh1
-rw-r--r--tests/bfs/O9.out19
-rw-r--r--tests/bfs/O9.sh4
-rw-r--r--tests/bfs/Ofast.out19
-rw-r--r--tests/bfs/Ofast.sh1
-rw-r--r--tests/bfs/S_bfs.out (renamed from tests/test_D_all.out)0
-rw-r--r--tests/bfs/S_bfs.sh2
-rw-r--r--tests/bfs/S_dfs.out19
-rw-r--r--tests/bfs/S_dfs.sh2
-rw-r--r--tests/bfs/S_ids.out (renamed from tests/test_D_multi.out)0
-rw-r--r--tests/bfs/S_ids.sh2
-rw-r--r--tests/bfs/and_incomplete.sh1
-rw-r--r--tests/bfs/capable.out1
-rw-r--r--tests/bfs/capable.sh10
-rw-r--r--tests/bfs/closed_stderr.sh4
-rw-r--r--tests/bfs/closed_stdin.out19
-rw-r--r--tests/bfs/closed_stdin.sh1
-rw-r--r--tests/bfs/closed_stdout.sh4
-rw-r--r--tests/bfs/color.out (renamed from tests/test_color.out)13
-rw-r--r--tests/bfs/color.sh1
-rw-r--r--tests/bfs/color_L.out (renamed from tests/test_color_L.out)13
-rw-r--r--tests/bfs/color_L.sh1
-rw-r--r--tests/bfs/color_L_ln_target.out (renamed from tests/test_color_ln_target.out)13
-rw-r--r--tests/bfs/color_L_ln_target.sh1
-rw-r--r--tests/bfs/color_L_no_stat.out (renamed from tests/test_color_L_no_stat.out)19
-rw-r--r--tests/bfs/color_L_no_stat.sh1
-rw-r--r--tests/bfs/color_auto.out (renamed from tests/test_color_ext_override.out)13
-rw-r--r--tests/bfs/color_auto.sh4
-rw-r--r--tests/bfs/color_cd0_no.out27
-rw-r--r--tests/bfs/color_cd0_no.sh1
-rw-r--r--tests/bfs/color_deep.out16
-rw-r--r--tests/bfs/color_deep.sh7
-rw-r--r--tests/bfs/color_escapes.out (renamed from tests/test_color_escapes.out)13
-rw-r--r--tests/bfs/color_escapes.sh1
-rw-r--r--tests/bfs/color_ext.out (renamed from tests/test_color_missing_colon.out)13
-rw-r--r--tests/bfs/color_ext.sh1
-rw-r--r--tests/bfs/color_ext0.out (renamed from tests/test_color_ext0.out)13
-rw-r--r--tests/bfs/color_ext0.sh1
-rw-r--r--tests/bfs/color_ext_case.out27
-rw-r--r--tests/bfs/color_ext_case.sh6
-rw-r--r--tests/bfs/color_ext_override.out27
-rw-r--r--tests/bfs/color_ext_override.sh1
-rw-r--r--tests/bfs/color_ext_underride.out27
-rw-r--r--tests/bfs/color_ext_underride.sh1
-rw-r--r--tests/bfs/color_fi0_no.out (renamed from tests/test_color_ext_underride.out)13
-rw-r--r--tests/bfs/color_fi0_no.sh1
-rw-r--r--tests/bfs/color_fi_no.out27
-rw-r--r--tests/bfs/color_fi_no.sh1
-rw-r--r--tests/bfs/color_ln_target.out (renamed from tests/test_color_L_ln_target.out)13
-rw-r--r--tests/bfs/color_ln_target.sh1
-rw-r--r--tests/bfs/color_ls.out12
-rw-r--r--tests/bfs/color_ls.sh15
-rw-r--r--tests/bfs/color_mh.out (renamed from tests/test_color_mh.out)13
-rw-r--r--tests/bfs/color_mh.sh1
-rw-r--r--tests/bfs/color_mh0.out (renamed from tests/test_color_mh0.out)13
-rw-r--r--tests/bfs/color_mh0.sh1
-rw-r--r--tests/bfs/color_mi.out27
-rw-r--r--tests/bfs/color_mi.sh1
-rw-r--r--tests/bfs/color_missing_colon.out (renamed from tests/test_color_ext.out)13
-rw-r--r--tests/bfs/color_missing_colon.sh1
-rw-r--r--tests/bfs/color_no.out27
-rw-r--r--tests/bfs/color_no.sh1
-rw-r--r--tests/bfs/color_no_stat.out (renamed from tests/test_color_no_stat.out)19
-rw-r--r--tests/bfs/color_no_stat.sh1
-rw-r--r--tests/bfs/color_nul.out27
-rw-r--r--tests/bfs/color_nul.sh3
-rw-r--r--tests/bfs/color_or.out (renamed from tests/test_color_or.out)13
-rw-r--r--tests/bfs/color_or.sh1
-rw-r--r--tests/bfs/color_or0_mi.out27
-rw-r--r--tests/bfs/color_or0_mi.sh1
-rw-r--r--tests/bfs/color_or0_mi0.out27
-rw-r--r--tests/bfs/color_or0_mi0.sh1
-rw-r--r--tests/bfs/color_or_mi.out (renamed from tests/test_color_or_mi.out)13
-rw-r--r--tests/bfs/color_or_mi.sh1
-rw-r--r--tests/bfs/color_or_mi0.out (renamed from tests/test_color_or_mi0.out)13
-rw-r--r--tests/bfs/color_or_mi0.sh1
-rw-r--r--tests/bfs/color_rs_lc_rc_ec.out (renamed from tests/test_color_rs_lc_rc_ec.out)15
-rw-r--r--tests/bfs/color_rs_lc_rc_ec.sh1
-rw-r--r--tests/bfs/color_st0_tw0_ow.out (renamed from tests/test_color_st0_tw0_ow.out)15
-rw-r--r--tests/bfs/color_st0_tw0_ow.sh1
-rw-r--r--tests/bfs/color_st0_tw0_ow0.out27
-rw-r--r--tests/bfs/color_st0_tw0_ow0.sh1
-rw-r--r--tests/bfs/color_st0_tw_ow.out (renamed from tests/test_color_st0_tw_ow.out)15
-rw-r--r--tests/bfs/color_st0_tw_ow.sh1
-rw-r--r--tests/bfs/color_st0_tw_ow0.out (renamed from tests/test_color_st0_tw_ow0.out)17
-rw-r--r--tests/bfs/color_st0_tw_ow0.sh1
-rw-r--r--tests/bfs/color_st_tw0_ow.out (renamed from tests/test_color_st_tw0_ow.out)13
-rw-r--r--tests/bfs/color_st_tw0_ow.sh1
-rw-r--r--tests/bfs/color_st_tw0_ow0.out (renamed from tests/test_color_st_tw0_ow0.out)15
-rw-r--r--tests/bfs/color_st_tw0_ow0.sh1
-rw-r--r--tests/bfs/color_st_tw_ow0.out (renamed from tests/test_color_st_tw_ow0.out)15
-rw-r--r--tests/bfs/color_st_tw_ow0.sh1
-rw-r--r--tests/bfs/color_star.sh2
-rw-r--r--tests/bfs/color_su0_sg.out (renamed from tests/test_color_su0_sg.out)13
-rw-r--r--tests/bfs/color_su0_sg.sh1
-rw-r--r--tests/bfs/color_su0_sg0.out (renamed from tests/test_color_su0_sg0.out)13
-rw-r--r--tests/bfs/color_su0_sg0.sh1
-rw-r--r--tests/bfs/color_su_sg0.out (renamed from tests/test_color_su_sg0.out)13
-rw-r--r--tests/bfs/color_su_sg0.sh1
-rw-r--r--tests/bfs/comma_incomplete.sh1
-rw-r--r--tests/bfs/data_flow_hidden.out19
-rw-r--r--tests/bfs/data_flow_hidden.sh1
-rw-r--r--tests/bfs/deep_strict.out (renamed from tests/test_deep.out)0
-rw-r--r--tests/bfs/deep_strict.sh3
-rw-r--r--tests/bfs/exclude_depth.out (renamed from tests/test_not_prune.out)6
-rw-r--r--tests/bfs/exclude_depth.sh1
-rw-r--r--tests/bfs/exclude_exclude.sh1
-rw-r--r--tests/bfs/exclude_mindepth.out (renamed from tests/test_L_ilname.out)0
-rw-r--r--tests/bfs/exclude_mindepth.sh1
-rw-r--r--tests/bfs/exclude_name.out (renamed from tests/test_prune_or_print.out)6
-rw-r--r--tests/bfs/exclude_name.sh1
-rw-r--r--tests/bfs/exclude_print.sh1
-rw-r--r--tests/bfs/exec_flush_fprint.out (renamed from tests/test_name_root.out)0
-rw-r--r--tests/bfs/exec_flush_fprint.sh2
-rw-r--r--tests/bfs/exec_flush_fprint_fail.sh2
-rw-r--r--tests/bfs/execdir_plus.out (renamed from tests/test_execdir_plus.out)2
-rw-r--r--tests/bfs/execdir_plus.sh4
-rw-r--r--tests/bfs/execdir_plus_nonexistent.out19
-rw-r--r--tests/bfs/execdir_plus_nonexistent.sh2
-rw-r--r--tests/bfs/expr_flag_path.out (renamed from tests/test_H_type_l.out)0
-rw-r--r--tests/bfs/expr_flag_path.sh1
-rw-r--r--tests/bfs/expr_path_flag.out (renamed from tests/test_expr_flag_path.out)0
-rw-r--r--tests/bfs/expr_path_flag.sh1
-rw-r--r--tests/bfs/flag_expr_path.out (renamed from tests/test_expr_path_flag.out)0
-rw-r--r--tests/bfs/flag_expr_path.sh1
-rw-r--r--tests/bfs/fprint_duplicate_stdout.out (renamed from tests/test_fprint_append.out)0
-rw-r--r--tests/bfs/fprint_duplicate_stdout.sh3
-rw-r--r--tests/bfs/fprint_error_stderr.sh2
-rw-r--r--tests/bfs/fprint_error_stdout.sh2
-rw-r--r--tests/bfs/help.sh4
-rw-r--r--tests/bfs/hidden.out (renamed from tests/test_hidden.out)0
-rw-r--r--tests/bfs/hidden.sh1
-rw-r--r--tests/bfs/hidden_root.out (renamed from tests/test_hidden_root.out)2
-rw-r--r--tests/bfs/hidden_root.sh2
-rw-r--r--tests/bfs/high_byte.sh1
-rw-r--r--tests/bfs/j0.sh1
-rw-r--r--tests/bfs/j1.out19
-rw-r--r--tests/bfs/j1.sh1
-rw-r--r--tests/bfs/j64.out19
-rw-r--r--tests/bfs/j64.sh1
-rw-r--r--tests/bfs/j_negative.sh1
-rw-r--r--tests/bfs/limit.out4
-rw-r--r--tests/bfs/limit.sh1
-rw-r--r--tests/bfs/limit_0.sh1
-rw-r--r--tests/bfs/limit_implicit_print.sh1
-rw-r--r--tests/bfs/limit_incomplete.sh1
-rw-r--r--tests/bfs/limit_one.sh1
-rw-r--r--tests/bfs/links_empty.sh1
-rw-r--r--tests/bfs/links_invalid.sh1
-rw-r--r--tests/bfs/links_leading_space.sh1
-rw-r--r--tests/bfs/links_negative.sh1
-rw-r--r--tests/bfs/links_noarg.sh1
-rw-r--r--tests/bfs/newerma_nonexistent.sh1
-rw-r--r--tests/bfs/newermq.sh1
-rw-r--r--tests/bfs/newermt_invalid.sh1
-rw-r--r--tests/bfs/newerqm.sh1
-rw-r--r--tests/bfs/nocolor.out27
-rw-r--r--tests/bfs/nocolor.sh1
-rw-r--r--tests/bfs/nocolor_env.out27
-rw-r--r--tests/bfs/nocolor_env.sh3
-rw-r--r--tests/bfs/nocolor_env_empty.out27
-rw-r--r--tests/bfs/nocolor_env_empty.sh3
-rw-r--r--tests/bfs/nohidden.out (renamed from tests/test_nohidden.out)16
-rw-r--r--tests/bfs/nohidden.sh1
-rw-r--r--tests/bfs/nohidden_depth.out (renamed from tests/test_nohidden_depth.out)16
-rw-r--r--tests/bfs/nohidden_depth.sh1
-rw-r--r--tests/bfs/nowarn.sh2
-rw-r--r--tests/bfs/ok_plus_semicolon.out (renamed from tests/test_ok_plus_semicolon.out)12
-rw-r--r--tests/bfs/ok_plus_semicolon.sh8
-rw-r--r--tests/bfs/okdir_plus_semicolon.out (renamed from tests/test_okdir_plus_semicolon.out)0
-rw-r--r--tests/bfs/okdir_plus_semicolon.sh1
-rw-r--r--tests/bfs/or_incomplete.sh1
-rw-r--r--tests/bfs/path_expr_flag.out (renamed from tests/test_flag_expr_path.out)0
-rw-r--r--tests/bfs/path_expr_flag.sh1
-rw-r--r--tests/bfs/path_flag_expr.out (renamed from tests/test_path_expr_flag.out)0
-rw-r--r--tests/bfs/path_flag_expr.sh1
-rw-r--r--tests/bfs/perm_leading_plus_symbolic.out (renamed from tests/test_L_lname.out)0
-rw-r--r--tests/bfs/perm_leading_plus_symbolic.sh1
-rw-r--r--tests/bfs/perm_symbolic_double_comma.sh1
-rw-r--r--tests/bfs/perm_symbolic_missing_action.sh1
-rw-r--r--tests/bfs/perm_symbolic_trailing_comma.sh1
-rw-r--r--tests/bfs/printf_color.out (renamed from tests/test_printf_color.out)18
-rw-r--r--tests/bfs/printf_color.sh1
-rw-r--r--tests/bfs/printf_duplicate_flag.sh1
-rw-r--r--tests/bfs/printf_everything.sh15
-rw-r--r--tests/bfs/printf_incomplete_escape.sh1
-rw-r--r--tests/bfs/printf_incomplete_format.sh1
-rw-r--r--tests/bfs/printf_invalid_escape.sh1
-rw-r--r--tests/bfs/printf_invalid_format.sh1
-rw-r--r--tests/bfs/printf_must_be_numeric.sh1
-rw-r--r--tests/bfs/printf_w.out (renamed from tests/test_and_purity.out)0
-rw-r--r--tests/bfs/printf_w.sh2
-rw-r--r--tests/bfs/status.sh1
-rw-r--r--tests/bfs/stderr_fails_loudly.sh2
-rw-r--r--tests/bfs/stderr_fails_silently.out19
-rw-r--r--tests/bfs/stderr_fails_silently.sh2
-rw-r--r--tests/bfs/type_multi.out (renamed from tests/test_type_multi.out)4
-rw-r--r--tests/bfs/type_multi.sh1
-rw-r--r--tests/bfs/typo.sh1
-rw-r--r--tests/bfs/unexpected_operator.sh1
-rw-r--r--tests/bfs/unique.out (renamed from tests/test_unique.out)0
-rw-r--r--tests/bfs/unique.sh1
-rw-r--r--tests/bfs/unique_depth.out19
-rw-r--r--tests/bfs/unique_depth.sh1
-rw-r--r--tests/bfs/version.sh1
-rw-r--r--tests/bfs/warn_O9.out19
-rw-r--r--tests/bfs/warn_O9.sh3
-rw-r--r--tests/bfs/warn_depth_prune.sh2
-rw-r--r--tests/bfs/warn_exclude_path.sh2
-rw-r--r--tests/bfs/warn_xdev_mount.out19
-rw-r--r--tests/bfs/warn_xdev_mount.sh2
-rw-r--r--tests/bfs/xtype_depth.sh2
-rw-r--r--tests/bfs/xtype_multi.out (renamed from tests/test_xtype_multi.out)8
-rw-r--r--tests/bfs/xtype_multi.sh1
-rw-r--r--tests/bfs/xtype_reorder.out (renamed from tests/test_data_flow_type.out)0
-rw-r--r--tests/bfs/xtype_reorder.sh4
-rw-r--r--tests/bfstd.c93
-rw-r--r--tests/bit.c141
-rw-r--r--tests/bsd/E.out (renamed from tests/test_E.out)0
-rw-r--r--tests/bsd/E.sh2
-rw-r--r--tests/bsd/H_mnewer.out (renamed from tests/test_H_mnewer.out)0
-rw-r--r--tests/bsd/H_mnewer.sh1
-rw-r--r--tests/bsd/L_acl.out2
-rw-r--r--tests/bsd/L_acl.sh9
-rw-r--r--tests/bsd/L_xattr.out3
-rw-r--r--tests/bsd/L_xattr.sh3
-rw-r--r--tests/bsd/L_xattrname.out2
-rw-r--r--tests/bsd/L_xattrname.sh11
-rw-r--r--tests/bsd/X.out (renamed from tests/test_X.out)14
-rw-r--r--tests/bsd/X.sh1
-rw-r--r--tests/bsd/acl.out1
-rw-r--r--tests/bsd/acl.sh9
-rw-r--r--tests/bsd/asince.out (renamed from tests/test_asince.out)0
-rw-r--r--tests/bsd/asince.sh1
-rw-r--r--tests/bsd/d_path.out19
-rw-r--r--tests/bsd/d_path.sh1
-rw-r--r--tests/bsd/data_flow_depth.out (renamed from tests/test_data_flow_depth.out)2
-rw-r--r--tests/bsd/data_flow_depth.sh1
-rw-r--r--tests/bsd/data_flow_sparse.out19
-rw-r--r--tests/bsd/data_flow_sparse.sh1
-rw-r--r--tests/bsd/depth_depth_n.out (renamed from tests/test_depth_depth_n.out)0
-rw-r--r--tests/bsd/depth_depth_n.sh1
-rw-r--r--tests/bsd/depth_depth_n_minus.out (renamed from tests/test_depth_depth_n_minus.out)0
-rw-r--r--tests/bsd/depth_depth_n_minus.sh1
-rw-r--r--tests/bsd/depth_depth_n_plus.out (renamed from tests/test_depth_depth_n_plus.out)0
-rw-r--r--tests/bsd/depth_depth_n_plus.sh1
-rw-r--r--tests/bsd/depth_n.out (renamed from tests/test_depth_n.out)0
-rw-r--r--tests/bsd/depth_n.sh1
-rw-r--r--tests/bsd/depth_n_minus.out (renamed from tests/test_depth_maxdepth_1.out)0
-rw-r--r--tests/bsd/depth_n_minus.sh1
-rw-r--r--tests/bsd/depth_n_plus.out (renamed from tests/test_depth_n_plus.out)0
-rw-r--r--tests/bsd/depth_n_plus.sh1
-rw-r--r--tests/bsd/depth_overflow.out19
-rw-r--r--tests/bsd/depth_overflow.sh1
-rw-r--r--tests/bsd/exit.out (renamed from tests/test_exit.out)0
-rw-r--r--tests/bsd/exit.sh5
-rw-r--r--tests/bsd/exit_no_implicit_print.out (renamed from tests/test_exclude_mindepth.out)0
-rw-r--r--tests/bsd/exit_no_implicit_print.sh1
-rw-r--r--tests/bsd/f.out (renamed from tests/test_f.out)2
-rw-r--r--tests/bsd/f.sh2
-rw-r--r--tests/bsd/f_incomplete.sh1
-rw-r--r--tests/bsd/flags.out1
-rw-r--r--tests/bsd/flags.sh8
-rw-r--r--tests/bsd/gid_name.out19
-rw-r--r--tests/bsd/gid_name.sh1
-rw-r--r--tests/bsd/mnewer.out (renamed from tests/test_H_newer.out)0
-rw-r--r--tests/bsd/mnewer.sh1
-rw-r--r--tests/bsd/msince.out (renamed from tests/test_msince.out)0
-rw-r--r--tests/bsd/msince.sh1
-rw-r--r--tests/bsd/mtime_bad_unit.sh1
-rw-r--r--tests/bsd/mtime_missing_unit.sh1
-rw-r--r--tests/bsd/mtime_units.out (renamed from tests/test_mtime_units.out)0
-rw-r--r--tests/bsd/mtime_units.sh1
-rw-r--r--tests/bsd/okdir_stdin.out (renamed from tests/test_okdir_stdin.out)0
-rw-r--r--tests/bsd/okdir_stdin.sh2
-rw-r--r--tests/bsd/perm_000_plus.out (renamed from tests/test_perm_000_minus.out)0
-rw-r--r--tests/bsd/perm_000_plus.sh1
-rw-r--r--tests/bsd/perm_222_plus.out (renamed from tests/test_perm_222_plus.out)0
-rw-r--r--tests/bsd/perm_222_plus.sh1
-rw-r--r--tests/bsd/perm_644_plus.out (renamed from tests/test_perm_644_plus.out)0
-rw-r--r--tests/bsd/perm_644_plus.sh1
-rw-r--r--tests/bsd/printx.out (renamed from tests/test_printx.out)18
-rw-r--r--tests/bsd/printx.sh1
-rw-r--r--tests/bsd/quit_implicit_print.out (renamed from tests/test_and_false_or_true.out)0
-rw-r--r--tests/bsd/quit_implicit_print.sh1
-rw-r--r--tests/bsd/rm.out1
-rw-r--r--tests/bsd/rm.sh4
-rw-r--r--tests/bsd/s.out (renamed from tests/test_s.out)1
-rw-r--r--tests/bsd/s.sh2
-rw-r--r--tests/bsd/s_quit.out1
-rw-r--r--tests/bsd/s_quit.sh4
-rw-r--r--tests/bsd/size_T.out (renamed from tests/test_size_T.out)0
-rw-r--r--tests/bsd/size_T.sh1
-rw-r--r--tests/bsd/sparse.out1
-rw-r--r--tests/bsd/sparse.sh12
-rw-r--r--tests/bsd/type_w.out34
-rw-r--r--tests/bsd/type_w.sh56
-rw-r--r--tests/bsd/uid_name.out19
-rw-r--r--tests/bsd/uid_name.sh1
-rw-r--r--tests/bsd/xattr.out3
-rw-r--r--tests/bsd/xattr.sh3
-rw-r--r--tests/bsd/xattrname.out2
-rw-r--r--tests/bsd/xattrname.sh11
-rw-r--r--tests/color.sh37
-rw-r--r--tests/common/H_newer.out (renamed from tests/test_anewer.out)0
-rw-r--r--tests/common/H_newer.sh1
-rw-r--r--tests/common/H_samefile_broken.out (renamed from tests/test_H_broken.out)0
-rw-r--r--tests/common/H_samefile_broken.sh1
-rw-r--r--tests/common/H_samefile_notdir.out (renamed from tests/test_H_notdir.out)0
-rw-r--r--tests/common/H_samefile_notdir.sh1
-rw-r--r--tests/common/H_samefile_symlink.out (renamed from tests/test_H_samefile_symlink.out)0
-rw-r--r--tests/common/H_samefile_symlink.sh1
-rw-r--r--tests/common/L_ilname.out (renamed from tests/test_false.out)0
-rw-r--r--tests/common/L_ilname.sh2
-rw-r--r--tests/common/L_lname.out (renamed from tests/test_ilname.out)0
-rw-r--r--tests/common/L_lname.sh1
-rw-r--r--tests/common/L_ls.sh1
-rw-r--r--tests/common/L_mount.out5
-rw-r--r--tests/common/L_mount.sh13
-rw-r--r--tests/common/L_samefile_broken.out (renamed from tests/test_H_samefile_broken.out)0
-rw-r--r--tests/common/L_samefile_broken.sh1
-rw-r--r--tests/common/L_samefile_notdir.out (renamed from tests/test_H_samefile_notdir.out)0
-rw-r--r--tests/common/L_samefile_notdir.sh1
-rw-r--r--tests/common/L_samefile_symlink.out (renamed from tests/test_L_samefile_symlink.out)0
-rw-r--r--tests/common/L_samefile_symlink.sh1
-rw-r--r--tests/common/P.out (renamed from tests/test_H.out)0
-rw-r--r--tests/common/P.sh1
-rw-r--r--tests/common/P_slash.out (renamed from tests/test_H_slash.out)0
-rw-r--r--tests/common/P_slash.sh1
-rw-r--r--tests/common/anewer.out (renamed from tests/test_mnewer.out)0
-rw-r--r--tests/common/anewer.sh1
-rw-r--r--tests/common/delete.out1
-rw-r--r--tests/common/delete.sh4
-rw-r--r--tests/common/delete_error.out8
-rw-r--r--tests/common/delete_error.sh9
-rw-r--r--tests/common/delete_many.out1
-rw-r--r--tests/common/delete_many.sh8
-rw-r--r--tests/common/depth_maxdepth_1.out (renamed from tests/test_depth_n_minus.out)0
-rw-r--r--tests/common/depth_maxdepth_1.sh1
-rw-r--r--tests/common/depth_maxdepth_2.out (renamed from tests/test_depth_maxdepth_2.out)10
-rw-r--r--tests/common/depth_maxdepth_2.sh1
-rw-r--r--tests/common/depth_mindepth_1.out (renamed from tests/test_depth_mindepth_1.out)12
-rw-r--r--tests/common/depth_mindepth_1.sh1
-rw-r--r--tests/common/depth_mindepth_2.out (renamed from tests/test_depth_mindepth_2.out)2
-rw-r--r--tests/common/depth_mindepth_2.sh1
-rw-r--r--tests/common/double_dash.out (renamed from tests/test_double_dash.out)0
-rw-r--r--tests/common/double_dash.sh2
-rw-r--r--tests/common/empty.out (renamed from tests/test_empty.out)2
-rw-r--r--tests/common/empty.sh1
-rw-r--r--tests/common/empty_error.out3
-rw-r--r--tests/common/empty_error.sh7
-rw-r--r--tests/common/empty_special.out20
-rw-r--r--tests/common/empty_special.sh1
-rw-r--r--tests/common/exec_substring.out (renamed from tests/test_exec_substring.out)12
-rw-r--r--tests/common/exec_substring.sh1
-rw-r--r--tests/common/execdir_nonexistent.out19
-rw-r--r--tests/common/execdir_nonexistent.sh2
-rw-r--r--tests/common/execdir_pwd.out (renamed from tests/test_execdir_pwd.out)2
-rw-r--r--tests/common/execdir_pwd.sh3
-rw-r--r--tests/common/execdir_slash.out (renamed from tests/test_execdir_slash.out)0
-rw-r--r--tests/common/execdir_slash.sh2
-rw-r--r--tests/common/execdir_slash_pwd.out (renamed from tests/test_execdir_slash_pwd.out)0
-rw-r--r--tests/common/execdir_slash_pwd.sh1
-rw-r--r--tests/common/execdir_slashes.out (renamed from tests/test_execdir_slashes.out)0
-rw-r--r--tests/common/execdir_slashes.sh1
-rw-r--r--tests/common/execdir_ulimit.out (renamed from tests/test_execdir_ulimit.out)2
-rw-r--r--tests/common/execdir_ulimit.sh6
-rw-r--r--tests/common/flag_double_dash.out (renamed from tests/test_flag_double_dash.out)0
-rw-r--r--tests/common/flag_double_dash.sh2
-rw-r--r--tests/common/follow.out (renamed from tests/test_L.out)12
-rw-r--r--tests/common/follow.sh1
-rw-r--r--tests/common/ilname.out (renamed from tests/test_lname.out)0
-rw-r--r--tests/common/ilname.sh2
-rw-r--r--tests/common/iname.out (renamed from tests/test_iname.out)0
-rw-r--r--tests/common/iname.sh2
-rw-r--r--tests/common/inum.out (renamed from tests/test_inum.out)0
-rw-r--r--tests/common/inum.sh1
-rw-r--r--tests/common/inum_bind_mount.out2
-rw-r--r--tests/common/inum_bind_mount.sh9
-rw-r--r--tests/common/inum_mount.out1
-rw-r--r--tests/common/inum_mount.sh9
-rw-r--r--tests/common/ipath.out (renamed from tests/test_path.out)2
-rw-r--r--tests/common/ipath.sh2
-rw-r--r--tests/common/iregex.out (renamed from tests/test_iregex.out)0
-rw-r--r--tests/common/iregex.sh1
-rw-r--r--tests/common/lname.out (renamed from tests/test_nogroup.out)0
-rw-r--r--tests/common/lname.sh1
-rw-r--r--tests/common/ls.sh1
-rw-r--r--tests/common/maxdepth.out (renamed from tests/test_maxdepth.out)0
-rw-r--r--tests/common/maxdepth.sh1
-rw-r--r--tests/common/maxdepth_incomplete.sh1
-rw-r--r--tests/common/mindepth.out (renamed from tests/test_mindepth.out)12
-rw-r--r--tests/common/mindepth.sh1
-rw-r--r--tests/common/mindepth_incomplete.sh1
-rw-r--r--tests/common/mount.out4
-rw-r--r--tests/common/mount.sh11
-rw-r--r--tests/common/name_slash.out (renamed from tests/test_name_slash.out)0
-rw-r--r--tests/common/name_slash.sh1
-rw-r--r--tests/common/name_slashes.out (renamed from tests/test_name_slashes.out)0
-rw-r--r--tests/common/name_slashes.sh1
-rw-r--r--tests/common/newerma.out (renamed from tests/test_newer.out)0
-rw-r--r--tests/common/newerma.sh1
-rw-r--r--tests/common/newermt.out (renamed from tests/test_newermt.out)0
-rw-r--r--tests/common/newermt.sh3
-rw-r--r--tests/common/newermt_epoch_minus_one.out (renamed from tests/test_newermt_epoch_minus_one.out)0
-rw-r--r--tests/common/newermt_epoch_minus_one.sh1
-rw-r--r--tests/common/ok_closed_stdin.out (renamed from tests/test_nogroup_ulimit.out)0
-rw-r--r--tests/common/ok_closed_stdin.sh1
-rw-r--r--tests/common/okdir_closed_stdin.out (renamed from tests/test_nouser.out)0
-rw-r--r--tests/common/okdir_closed_stdin.sh1
-rw-r--r--tests/common/quit.out (renamed from tests/test_name_root_depth.out)0
-rw-r--r--tests/common/quit.sh1
-rw-r--r--tests/common/quit_after_print.out (renamed from tests/test_comma_reachability.out)0
-rw-r--r--tests/common/quit_after_print.sh1
-rw-r--r--tests/common/quit_before_print.out (renamed from tests/test_nouser_ulimit.out)0
-rw-r--r--tests/common/quit_before_print.sh1
-rw-r--r--tests/common/quit_child.out (renamed from tests/test_quit_child.out)0
-rw-r--r--tests/common/quit_child.sh1
-rw-r--r--tests/common/quit_depth.out (renamed from tests/test_quit_depth.out)0
-rw-r--r--tests/common/quit_depth.sh1
-rw-r--r--tests/common/quit_depth_child.out (renamed from tests/test_quit_depth_child.out)0
-rw-r--r--tests/common/quit_depth_child.sh1
-rw-r--r--tests/common/regex.out (renamed from tests/test_regex.out)0
-rw-r--r--tests/common/regex.sh1
-rw-r--r--tests/common/regex_parens.out (renamed from tests/test_regex_parens.out)0
-rw-r--r--tests/common/regex_parens.sh2
-rw-r--r--tests/common/samefile.out (renamed from tests/test_links.out)0
-rw-r--r--tests/common/samefile.sh1
-rw-r--r--tests/common/samefile_broken.out (renamed from tests/test_L_broken.out)0
-rw-r--r--tests/common/samefile_broken.sh1
-rw-r--r--tests/common/samefile_notdir.out (renamed from tests/test_L_notdir.out)0
-rw-r--r--tests/common/samefile_notdir.sh1
-rw-r--r--tests/common/samefile_symlink.out (renamed from tests/test_samefile_symlink.out)0
-rw-r--r--tests/common/samefile_symlink.sh1
-rw-r--r--tests/common/samefile_wordesc.sh4
-rw-r--r--tests/common/size_big.out (renamed from tests/test_ok_closed_stdin.out)0
-rw-r--r--tests/common/size_big.sh1
-rwxr-xr-xtests/find-color.sh17
-rw-r--r--tests/getopts.sh179
-rw-r--r--tests/gnu/L_delete.out2
-rw-r--r--tests/gnu/L_delete.sh8
-rw-r--r--tests/gnu/L_loops_continue.out (renamed from tests/test_L_loops_continue.out)6
-rw-r--r--tests/gnu/L_loops_continue.sh1
-rw-r--r--tests/gnu/L_xtype_f.out (renamed from tests/test_L_xtype_f.out)2
-rw-r--r--tests/gnu/L_xtype_f.sh1
-rw-r--r--tests/gnu/L_xtype_l.out (renamed from tests/test_L_xtype_l.out)6
-rw-r--r--tests/gnu/L_xtype_l.sh1
-rw-r--r--tests/gnu/and.out (renamed from tests/test_a.out)0
-rw-r--r--tests/gnu/and.sh1
-rw-r--r--tests/gnu/and_false_or_true.out (renamed from tests/test_comma_redundant_false.out)0
-rw-r--r--tests/gnu/and_false_or_true.sh3
-rw-r--r--tests/gnu/and_purity.out (renamed from tests/test_okdir_closed_stdin.out)0
-rw-r--r--tests/gnu/and_purity.sh2
-rw-r--r--tests/gnu/comma.out (renamed from tests/test_comma.out)14
-rw-r--r--tests/gnu/comma.sh1
-rw-r--r--tests/gnu/comma_reachability.out (renamed from tests/test_comma_redundant_true.out)0
-rw-r--r--tests/gnu/comma_reachability.sh1
-rw-r--r--tests/gnu/comma_redundant_false.out (renamed from tests/test_fprint_truncate.out)0
-rw-r--r--tests/gnu/comma_redundant_false.sh2
-rw-r--r--tests/gnu/comma_redundant_true.out (renamed from tests/test_not_reachability.out)0
-rw-r--r--tests/gnu/comma_redundant_true.sh2
-rw-r--r--tests/gnu/daystart.out19
-rw-r--r--tests/gnu/daystart.sh1
-rw-r--r--tests/gnu/daystart_twice.out19
-rw-r--r--tests/gnu/daystart_twice.sh1
-rw-r--r--tests/gnu/exec_flush.out19
-rw-r--r--tests/gnu/exec_flush.sh4
-rw-r--r--tests/gnu/exec_flush_fail.sh3
-rw-r--r--tests/gnu/exec_nothing.sh2
-rw-r--r--tests/gnu/exec_plus_flush.outbin0 -> 22 bytes
-rw-r--r--tests/gnu/exec_plus_flush.sh2
-rw-r--r--tests/gnu/exec_plus_flush_fail.sh2
-rw-r--r--tests/gnu/execdir.out (renamed from tests/test_execdir.out)0
-rw-r--r--tests/gnu/execdir.sh1
-rw-r--r--tests/gnu/execdir_path_dot.sh1
-rw-r--r--tests/gnu/execdir_path_empty.sh1
-rw-r--r--tests/gnu/execdir_path_relative.sh1
-rw-r--r--tests/gnu/execdir_plus_semicolon.out (renamed from tests/test_execdir_plus_semicolon.out)0
-rw-r--r--tests/gnu/execdir_plus_semicolon.sh1
-rw-r--r--tests/gnu/execdir_substring.out (renamed from tests/test_execdir_substring.out)0
-rw-r--r--tests/gnu/execdir_substring.sh1
-rw-r--r--tests/gnu/execdir_ulimit.out16
-rw-r--r--tests/gnu/execdir_ulimit.sh2
-rw-r--r--tests/gnu/executable.out (renamed from tests/test_executable.out)0
-rw-r--r--tests/gnu/executable.sh1
-rw-r--r--tests/gnu/false.out (renamed from tests/test_or_purity.out)0
-rw-r--r--tests/gnu/false.sh1
-rw-r--r--tests/gnu/files0_from_empty.sh1
-rw-r--r--tests/gnu/files0_from_error.sh1
-rw-r--r--tests/gnu/files0_from_file.out (renamed from tests/test_files0_from_file.out)21
-rw-r--r--tests/gnu/files0_from_file.sh4
-rw-r--r--tests/gnu/files0_from_none.out (renamed from tests/test_perm_leading_plus_symbolic.out)0
-rw-r--r--tests/gnu/files0_from_none.sh1
-rw-r--r--tests/gnu/files0_from_nothing.sh1
-rw-r--r--tests/gnu/files0_from_nowhere.sh1
-rw-r--r--tests/gnu/files0_from_ok.sh1
-rw-r--r--tests/gnu/files0_from_stdin.out (renamed from tests/test_files0_from_stdin.out)21
-rw-r--r--tests/gnu/files0_from_stdin.sh2
-rw-r--r--tests/gnu/fls.sh1
-rw-r--r--tests/gnu/fls_nonexistent.sh1
-rw-r--r--tests/gnu/follow_comma.out (renamed from tests/test_follow_comma.out)18
-rw-r--r--tests/gnu/follow_comma.sh3
-rw-r--r--tests/gnu/fprint.out19
-rw-r--r--tests/gnu/fprint.sh3
-rw-r--r--tests/gnu/fprint0.out (renamed from tests/test_fprint0.out)bin16 -> 16 bytes
-rw-r--r--tests/gnu/fprint0.sh2
-rw-r--r--tests/gnu/fprint0_nonexistent.sh1
-rw-r--r--tests/gnu/fprint_duplicate.out (renamed from tests/test_fprint_duplicate.out)0
-rw-r--r--tests/gnu/fprint_duplicate.sh7
-rw-r--r--tests/gnu/fprint_error.sh2
-rw-r--r--tests/gnu/fprint_noarg.sh1
-rw-r--r--tests/gnu/fprint_noerror.sh3
-rw-r--r--tests/gnu/fprint_nonexistent.sh1
-rw-r--r--tests/gnu/fprint_truncate.out (renamed from tests/test_printf_leak.out)0
-rw-r--r--tests/gnu/fprint_truncate.sh5
-rw-r--r--tests/gnu/fprintf.out (renamed from tests/test_fprintf.out)0
-rw-r--r--tests/gnu/fprintf.sh3
-rw-r--r--tests/gnu/fprintf_nofile.sh1
-rw-r--r--tests/gnu/fprintf_noformat.sh1
-rw-r--r--tests/gnu/fprintf_nonexistent.sh1
-rw-r--r--tests/gnu/fstype.out19
-rw-r--r--tests/gnu/fstype.sh2
-rw-r--r--tests/gnu/fstype_stacked.out1
-rw-r--r--tests/gnu/fstype_stacked.sh12
-rw-r--r--tests/gnu/fstype_umount.out (renamed from tests/test_perm_leading_plus_symbolic_minus.out)0
-rw-r--r--tests/gnu/fstype_umount.sh12
-rw-r--r--tests/gnu/gid.out19
-rw-r--r--tests/gnu/gid.sh1
-rw-r--r--tests/gnu/gid_minus.out19
-rw-r--r--tests/gnu/gid_minus.sh1
-rw-r--r--tests/gnu/gid_minus_plus.out19
-rw-r--r--tests/gnu/gid_minus_plus.sh1
-rw-r--r--tests/gnu/gid_plus.out19
-rw-r--r--tests/gnu/gid_plus.sh2
-rw-r--r--tests/gnu/gid_plus_plus.out19
-rw-r--r--tests/gnu/gid_plus_plus.sh2
-rw-r--r--tests/gnu/ignore_readdir_race.sh5
-rw-r--r--tests/gnu/ignore_readdir_race_notdir.sh7
-rw-r--r--tests/gnu/ignore_readdir_race_root.sh2
-rw-r--r--tests/gnu/inum_automount.out1
-rw-r--r--tests/gnu/inum_automount.sh14
-rw-r--r--tests/gnu/iwholename.out (renamed from tests/test_ipath.out)2
-rw-r--r--tests/gnu/iwholename.sh2
-rw-r--r--tests/gnu/newer_link.out (renamed from tests/test_newer_link.out)0
-rw-r--r--tests/gnu/newer_link.sh1
-rw-r--r--tests/gnu/noleaf.out19
-rw-r--r--tests/gnu/noleaf.sh1
-rw-r--r--tests/gnu/not.out (renamed from tests/test_bang.out)8
-rw-r--r--tests/gnu/not.sh1
-rw-r--r--tests/gnu/not_reachability.out (renamed from tests/test_quit_after_print.out)0
-rw-r--r--tests/gnu/not_reachability.sh1
-rw-r--r--tests/gnu/ok_flush.out19
-rw-r--r--tests/gnu/ok_flush.sh4
-rw-r--r--tests/gnu/ok_nothing.sh2
-rw-r--r--tests/gnu/okdir_path_dot.sh1
-rw-r--r--tests/gnu/okdir_path_empty.sh1
-rw-r--r--tests/gnu/okdir_path_relative.sh1
-rw-r--r--tests/gnu/or.out (renamed from tests/test_o.out)6
-rw-r--r--tests/gnu/or.sh1
-rw-r--r--tests/gnu/path_d.out19
-rw-r--r--tests/gnu/path_d.sh1
-rw-r--r--tests/gnu/perm_000_slash.out (renamed from tests/test_perm_000_plus.out)0
-rw-r--r--tests/gnu/perm_000_slash.sh1
-rw-r--r--tests/gnu/perm_222_slash.out (renamed from tests/test_perm_222_slash.out)0
-rw-r--r--tests/gnu/perm_222_slash.sh1
-rw-r--r--tests/gnu/perm_644_slash.out (renamed from tests/test_perm_644_slash.out)0
-rw-r--r--tests/gnu/perm_644_slash.sh1
-rw-r--r--tests/gnu/perm_leading_plus_symbolic_slash.out (renamed from tests/test_perm_leading_plus_symbolic_slash.out)0
-rw-r--r--tests/gnu/perm_leading_plus_symbolic_slash.sh1
-rw-r--r--tests/gnu/perm_symbolic_slash.out (renamed from tests/test_perm_symbolic_slash.out)0
-rw-r--r--tests/gnu/perm_symbolic_slash.sh1
-rw-r--r--tests/gnu/precedence.out (renamed from tests/test_precedence.out)2
-rw-r--r--tests/gnu/precedence.sh1
-rw-r--r--tests/gnu/print0.out (renamed from tests/test_print0.out)bin16 -> 16 bytes
-rw-r--r--tests/gnu/print0.sh2
-rw-r--r--tests/gnu/print_error.sh2
-rw-r--r--tests/gnu/printf.out (renamed from tests/test_printf.out)12
-rw-r--r--tests/gnu/printf.sh1
-rw-r--r--tests/gnu/printf_H.out (renamed from tests/test_printf_H.out)30
-rw-r--r--tests/gnu/printf_H.sh1
-rw-r--r--tests/gnu/printf_Y_error.out3
-rw-r--r--tests/gnu/printf_Y_error.sh8
-rw-r--r--tests/gnu/printf_empty.out (renamed from tests/test_perm_symbolic.out)0
-rw-r--r--tests/gnu/printf_empty.sh1
-rw-r--r--tests/gnu/printf_escapes.out (renamed from tests/test_printf_escapes.out)0
-rw-r--r--tests/gnu/printf_escapes.sh1
-rw-r--r--tests/gnu/printf_flags.out (renamed from tests/test_printf_flags.out)10
-rw-r--r--tests/gnu/printf_flags.sh1
-rw-r--r--tests/gnu/printf_l_nonlink.out (renamed from tests/test_printf_l_nonlink.out)4
-rw-r--r--tests/gnu/printf_l_nonlink.sh1
-rw-r--r--tests/gnu/printf_leak.out (renamed from tests/test_quit_implicit_print.out)0
-rw-r--r--tests/gnu/printf_leak.sh2
-rw-r--r--tests/gnu/printf_nul.outbin0 -> 16 bytes
-rw-r--r--tests/gnu/printf_nul.sh3
-rw-r--r--tests/gnu/printf_slash.out (renamed from tests/test_printf_slash.out)0
-rw-r--r--tests/gnu/printf_slash.sh1
-rw-r--r--tests/gnu/printf_slashes.out (renamed from tests/test_printf_slashes.out)0
-rw-r--r--tests/gnu/printf_slashes.sh1
-rw-r--r--tests/gnu/printf_times.out (renamed from tests/test_printf_times.out)0
-rw-r--r--tests/gnu/printf_times.sh1
-rw-r--r--tests/gnu/printf_trailing_slash.out (renamed from tests/test_printf_trailing_slash.out)4
-rw-r--r--tests/gnu/printf_trailing_slash.sh1
-rw-r--r--tests/gnu/printf_trailing_slashes.out (renamed from tests/test_printf_trailing_slashes.out)4
-rw-r--r--tests/gnu/printf_trailing_slashes.sh1
-rw-r--r--tests/gnu/printf_types.out (renamed from tests/test_printf_types.out)8
-rw-r--r--tests/gnu/printf_types.sh1
-rw-r--r--tests/gnu/printf_u_g_ulimit.sh2
-rw-r--r--tests/gnu/readable.out (renamed from tests/test_readable.out)0
-rw-r--r--tests/gnu/readable.sh1
-rw-r--r--tests/gnu/regex_error.sh1
-rw-r--r--tests/gnu/regex_invalid_utf8.out1
-rw-r--r--tests/gnu/regex_invalid_utf8.sh8
-rw-r--r--tests/gnu/regextype_ed.out (renamed from tests/test_regextype_posix_basic.out)0
-rw-r--r--tests/gnu/regextype_ed.sh2
-rw-r--r--tests/gnu/regextype_emacs.out6
-rw-r--r--tests/gnu/regextype_emacs.sh3
-rw-r--r--tests/gnu/regextype_grep.out (renamed from tests/test_name.out)0
-rw-r--r--tests/gnu/regextype_grep.sh3
-rw-r--r--tests/gnu/regextype_posix_basic.out (renamed from tests/test_regextype_posix_extended.out)0
-rw-r--r--tests/gnu/regextype_posix_basic.sh2
-rw-r--r--tests/gnu/regextype_posix_extended.out1
-rw-r--r--tests/gnu/regextype_posix_extended.sh2
-rw-r--r--tests/gnu/regextype_sed.out1
-rw-r--r--tests/gnu/regextype_sed.sh2
-rw-r--r--tests/gnu/true.out19
-rw-r--r--tests/gnu/true.sh1
-rw-r--r--tests/gnu/uid.out19
-rw-r--r--tests/gnu/uid.sh1
-rw-r--r--tests/gnu/uid_minus.out19
-rw-r--r--tests/gnu/uid_minus.sh1
-rw-r--r--tests/gnu/uid_minus_plus.out19
-rw-r--r--tests/gnu/uid_minus_plus.sh1
-rw-r--r--tests/gnu/uid_plus.out19
-rw-r--r--tests/gnu/uid_plus.sh2
-rw-r--r--tests/gnu/uid_plus_plus.out19
-rw-r--r--tests/gnu/uid_plus_plus.sh2
-rw-r--r--tests/gnu/used.out4
-rw-r--r--tests/gnu/used.sh40
-rw-r--r--tests/gnu/wholename.out7
-rw-r--r--tests/gnu/wholename.sh1
-rw-r--r--tests/gnu/writable.out (renamed from tests/test_writable.out)0
-rw-r--r--tests/gnu/writable.sh1
-rw-r--r--tests/gnu/xtype_bind_mount.out2
-rw-r--r--tests/gnu/xtype_bind_mount.sh10
-rw-r--r--tests/gnu/xtype_f.out (renamed from tests/test_xtype_f.out)4
-rw-r--r--tests/gnu/xtype_f.sh1
-rw-r--r--tests/gnu/xtype_l.out (renamed from tests/test_xtype_l.out)2
-rw-r--r--tests/gnu/xtype_l.sh1
-rw-r--r--tests/ioq.c77
-rwxr-xr-xtests/ls-color.sh54
-rw-r--r--tests/main.c125
-rw-r--r--tests/mksock.c45
-rw-r--r--tests/posix/H.out (renamed from tests/test_P.out)0
-rw-r--r--tests/posix/H.sh1
-rw-r--r--tests/posix/H_broken.out (renamed from tests/test_L_samefile_broken.out)0
-rw-r--r--tests/posix/H_broken.sh1
-rw-r--r--tests/posix/H_loops.out (renamed from tests/test_H_loops.out)0
-rw-r--r--tests/posix/H_loops.sh1
-rw-r--r--tests/posix/H_notdir.out (renamed from tests/test_L_samefile_notdir.out)0
-rw-r--r--tests/posix/H_notdir.sh1
-rw-r--r--tests/posix/H_slash.out (renamed from tests/test_P_slash.out)0
-rw-r--r--tests/posix/H_slash.sh1
-rw-r--r--tests/posix/H_type_l.out (renamed from tests/test_path_flag_expr.out)0
-rw-r--r--tests/posix/H_type_l.sh1
-rw-r--r--tests/posix/L.out (renamed from tests/test_L_depth.out)12
-rw-r--r--tests/posix/L.sh1
-rw-r--r--tests/posix/L_broken.out (renamed from tests/test_samefile_broken.out)0
-rw-r--r--tests/posix/L_broken.sh1
-rw-r--r--tests/posix/L_depth.out (renamed from tests/test_follow.out)12
-rw-r--r--tests/posix/L_depth.sh1
-rw-r--r--tests/posix/L_loops.sh4
-rw-r--r--tests/posix/L_notdir.out (renamed from tests/test_samefile_notdir.out)0
-rw-r--r--tests/posix/L_notdir.sh1
-rw-r--r--tests/posix/L_type_l.out (renamed from tests/test_L_type_l.out)0
-rw-r--r--tests/posix/L_type_l.sh1
-rw-r--r--tests/posix/L_xdev.out5
-rw-r--r--tests/posix/L_xdev.sh13
-rw-r--r--tests/posix/a.out (renamed from tests/test_and.out)0
-rw-r--r--tests/posix/a.sh1
-rw-r--r--tests/posix/bang.out (renamed from tests/test_not.out)8
-rw-r--r--tests/posix/bang.sh1
-rw-r--r--tests/posix/basic.out19
-rw-r--r--tests/posix/basic.sh1
-rw-r--r--tests/posix/data_flow_and_swap.out (renamed from tests/test_data_flow_and_swap.out)4
-rw-r--r--tests/posix/data_flow_and_swap.sh1
-rw-r--r--tests/posix/data_flow_group.out19
-rw-r--r--tests/posix/data_flow_group.sh1
-rw-r--r--tests/posix/data_flow_or_swap.out (renamed from tests/test_data_flow_or_swap.out)4
-rw-r--r--tests/posix/data_flow_or_swap.sh1
-rw-r--r--tests/posix/data_flow_type.out (renamed from tests/test_printf_empty.out)0
-rw-r--r--tests/posix/data_flow_type.sh1
-rw-r--r--tests/posix/data_flow_user.out19
-rw-r--r--tests/posix/data_flow_user.sh1
-rw-r--r--tests/posix/de_morgan_and.out (renamed from tests/test_de_morgan_and.out)2
-rw-r--r--tests/posix/de_morgan_and.sh1
-rw-r--r--tests/posix/de_morgan_not.out (renamed from tests/test_de_morgan_not.out)0
-rw-r--r--tests/posix/de_morgan_not.sh1
-rw-r--r--tests/posix/de_morgan_or.out (renamed from tests/test_de_morgan_or.out)10
-rw-r--r--tests/posix/de_morgan_or.sh1
-rw-r--r--tests/posix/deep.out (renamed from tests/test_deep_strict.out)0
-rw-r--r--tests/posix/deep.sh2
-rw-r--r--tests/posix/depth.out19
-rw-r--r--tests/posix/depth.sh1
-rw-r--r--tests/posix/depth_error.out2
-rw-r--r--tests/posix/depth_error.sh7
-rw-r--r--tests/posix/depth_slash.out (renamed from tests/test_depth_slash.out)12
-rw-r--r--tests/posix/depth_slash.sh1
-rw-r--r--tests/posix/double_negation.out (renamed from tests/test_double_negation.out)0
-rw-r--r--tests/posix/double_negation.sh1
-rw-r--r--tests/posix/exec.out19
-rw-r--r--tests/posix/exec.sh1
-rw-r--r--tests/posix/exec_nonexistent.out19
-rw-r--r--tests/posix/exec_nonexistent.sh4
-rw-r--r--tests/posix/exec_nopath.out19
-rw-r--r--tests/posix/exec_nopath.sh7
-rw-r--r--tests/posix/exec_plus.out (renamed from tests/test_exec_plus.out)0
-rw-r--r--tests/posix/exec_plus.sh1
-rw-r--r--tests/posix/exec_plus_nonexistent.out19
-rw-r--r--tests/posix/exec_plus_nonexistent.sh2
-rw-r--r--tests/posix/exec_plus_nothing.sh2
-rw-r--r--tests/posix/exec_plus_semicolon.out (renamed from tests/test_exec_plus_semicolon.out)12
-rw-r--r--tests/posix/exec_plus_semicolon.sh5
-rw-r--r--tests/posix/exec_plus_status.out19
-rw-r--r--tests/posix/exec_plus_status.sh3
-rw-r--r--tests/posix/exec_ulimit.out16
-rw-r--r--tests/posix/exec_ulimit.sh2
-rw-r--r--tests/posix/extra_paren.sh1
-rw-r--r--tests/posix/flag_comma.out (renamed from tests/test_flag_comma.out)0
-rw-r--r--tests/posix/flag_comma.sh3
-rw-r--r--tests/posix/flag_weird_names.out (renamed from tests/test_flag_weird_names.out)20
-rw-r--r--tests/posix/flag_weird_names.sh2
-rw-r--r--tests/posix/group_id.out19
-rw-r--r--tests/posix/group_id.sh1
-rw-r--r--tests/posix/group_name.out19
-rw-r--r--tests/posix/group_name.sh1
-rw-r--r--tests/posix/group_nogroup.out19
-rw-r--r--tests/posix/group_nogroup.sh2
-rw-r--r--tests/posix/implicit_and.out (renamed from tests/test_implicit_and.out)0
-rw-r--r--tests/posix/implicit_and.sh1
-rw-r--r--tests/posix/incomplete.sh1
-rw-r--r--tests/posix/links.out (renamed from tests/test_links_plus.out)0
-rw-r--r--tests/posix/links.sh1
-rw-r--r--tests/posix/links_minus.out (renamed from tests/test_links_minus.out)0
-rw-r--r--tests/posix/links_minus.sh1
-rw-r--r--tests/posix/links_plus.out (renamed from tests/test_samefile.out)0
-rw-r--r--tests/posix/links_plus.sh1
-rw-r--r--tests/posix/missing_paren.sh1
-rw-r--r--tests/posix/name.out (renamed from tests/test_parens.out)0
-rw-r--r--tests/posix/name.sh1
-rw-r--r--tests/posix/name_backslash.out (renamed from tests/test_printf_w.out)0
-rw-r--r--tests/posix/name_backslash.sh2
-rw-r--r--tests/posix/name_bracket.out1
-rw-r--r--tests/posix/name_bracket.sh9
-rw-r--r--tests/posix/name_character_class.out (renamed from tests/test_prune.out)0
-rw-r--r--tests/posix/name_character_class.sh1
-rw-r--r--tests/posix/name_double_backslash.out1
-rw-r--r--tests/posix/name_double_backslash.sh2
-rw-r--r--tests/posix/name_root.out1
-rw-r--r--tests/posix/name_root.sh1
-rw-r--r--tests/posix/name_root_depth.out (renamed from tests/test_quit.out)0
-rw-r--r--tests/posix/name_root_depth.sh1
-rw-r--r--tests/posix/name_star_star.out4
-rw-r--r--tests/posix/name_star_star.sh1
-rw-r--r--tests/posix/name_trailing_slash.out (renamed from tests/test_name_trailing_slash.out)0
-rw-r--r--tests/posix/name_trailing_slash.sh1
-rw-r--r--tests/posix/newer.out (renamed from tests/test_newerma.out)0
-rw-r--r--tests/posix/newer.sh1
-rw-r--r--tests/posix/newer_broken.out1
-rw-r--r--tests/posix/newer_broken.sh4
-rw-r--r--tests/posix/newer_nonexistent.sh1
-rw-r--r--tests/posix/nogroup.out (renamed from tests/test_quit_before_print.out)0
-rw-r--r--tests/posix/nogroup.sh1
-rw-r--r--tests/posix/nogroup_ulimit.out (renamed from tests/test_size_big.out)0
-rw-r--r--tests/posix/nogroup_ulimit.sh2
-rw-r--r--tests/posix/not_prune.out (renamed from tests/test_exclude_depth.out)6
-rw-r--r--tests/posix/not_prune.sh1
-rw-r--r--tests/posix/nouser.out (renamed from tests/test_xtype_reorder.out)0
-rw-r--r--tests/posix/nouser.sh1
-rw-r--r--tests/posix/nouser_ulimit.out0
-rw-r--r--tests/posix/nouser_ulimit.sh2
-rw-r--r--tests/posix/o.out (renamed from tests/test_or.out)6
-rw-r--r--tests/posix/o.sh1
-rw-r--r--tests/posix/ok_plus_nothing.sh2
-rw-r--r--tests/posix/ok_stdin.out (renamed from tests/test_ok_stdin.out)20
-rw-r--r--tests/posix/ok_stdin.sh3
-rw-r--r--tests/posix/or_purity.out0
-rw-r--r--tests/posix/or_purity.sh2
-rw-r--r--tests/posix/overlayfs.out5
-rw-r--r--tests/posix/overlayfs.sh11
-rw-r--r--tests/posix/parens.out4
-rw-r--r--tests/posix/parens.sh1
-rw-r--r--tests/posix/path.out7
-rw-r--r--tests/posix/path.sh1
-rw-r--r--tests/posix/perm_000.out (renamed from tests/test_perm_000.out)0
-rw-r--r--tests/posix/perm_000.sh1
-rw-r--r--tests/posix/perm_000_minus.out (renamed from tests/test_perm_000_slash.out)0
-rw-r--r--tests/posix/perm_000_minus.sh1
-rw-r--r--tests/posix/perm_222.out (renamed from tests/test_perm_222.out)0
-rw-r--r--tests/posix/perm_222.sh1
-rw-r--r--tests/posix/perm_222_minus.out (renamed from tests/test_perm_222_minus.out)0
-rw-r--r--tests/posix/perm_222_minus.sh1
-rw-r--r--tests/posix/perm_644.out (renamed from tests/test_perm_644.out)0
-rw-r--r--tests/posix/perm_644.sh1
-rw-r--r--tests/posix/perm_644_minus.out (renamed from tests/test_perm_644_minus.out)0
-rw-r--r--tests/posix/perm_644_minus.sh1
-rw-r--r--tests/posix/perm_leading_plus_symbolic_minus.out0
-rw-r--r--tests/posix/perm_leading_plus_symbolic_minus.sh1
-rw-r--r--tests/posix/perm_setid.out (renamed from tests/test_perm_setid.out)0
-rw-r--r--tests/posix/perm_setid.sh1
-rw-r--r--tests/posix/perm_sticky.out (renamed from tests/test_perm_sticky.out)0
-rw-r--r--tests/posix/perm_sticky.sh1
-rw-r--r--tests/posix/perm_symbolic.out0
-rw-r--r--tests/posix/perm_symbolic.sh1
-rw-r--r--tests/posix/perm_symbolic_minus.out (renamed from tests/test_perm_symbolic_minus.out)0
-rw-r--r--tests/posix/perm_symbolic_minus.sh1
-rw-r--r--tests/posix/permcopy.out (renamed from tests/test_permcopy.out)0
-rw-r--r--tests/posix/permcopy.sh1
-rw-r--r--tests/posix/prune.out3
-rw-r--r--tests/posix/prune.sh1
-rw-r--r--tests/posix/prune_file.out10
-rw-r--r--tests/posix/prune_file.sh1
-rw-r--r--tests/posix/prune_or_print.out (renamed from tests/test_exclude_name.out)6
-rw-r--r--tests/posix/prune_or_print.sh1
-rw-r--r--tests/posix/readdir_error.sh37
-rw-r--r--tests/posix/size.out (renamed from tests/test_size.out)0
-rw-r--r--tests/posix/size.sh1
-rw-r--r--tests/posix/size_bytes.out (renamed from tests/test_size_bytes.out)0
-rw-r--r--tests/posix/size_bytes.sh1
-rw-r--r--tests/posix/size_plus.out (renamed from tests/test_size_plus.out)0
-rw-r--r--tests/posix/size_plus.sh1
-rw-r--r--tests/posix/type_bind_mount.out1
-rw-r--r--tests/posix/type_bind_mount.sh9
-rw-r--r--tests/posix/type_d.out (renamed from tests/test_type_d.out)4
-rw-r--r--tests/posix/type_d.sh1
-rw-r--r--tests/posix/type_f.out7
-rw-r--r--tests/posix/type_f.sh1
-rw-r--r--tests/posix/type_l.out (renamed from tests/test_type_l.out)0
-rw-r--r--tests/posix/type_l.sh1
-rw-r--r--tests/posix/unionfs.out10
-rw-r--r--tests/posix/unionfs.sh9
-rw-r--r--tests/posix/user_id.out19
-rw-r--r--tests/posix/user_id.sh1
-rw-r--r--tests/posix/user_name.out19
-rw-r--r--tests/posix/user_name.sh1
-rw-r--r--tests/posix/user_nouser.out19
-rw-r--r--tests/posix/user_nouser.sh2
-rw-r--r--tests/posix/weird_names.out (renamed from tests/test_weird_names.out)20
-rw-r--r--tests/posix/weird_names.sh2
-rw-r--r--tests/posix/xdev.out4
-rw-r--r--tests/posix/xdev.sh11
-rw-r--r--tests/run.sh464
-rwxr-xr-xtests/sort-args.sh2
-rw-r--r--tests/stddirs.sh152
-rw-r--r--tests/test_L_acl.out2
-rw-r--r--tests/test_L_capable.out2
-rw-r--r--tests/test_L_delete.out2
-rw-r--r--tests/test_L_mount.out5
-rw-r--r--tests/test_L_xattr.out3
-rw-r--r--tests/test_L_xattrname.out2
-rw-r--r--tests/test_L_xdev.out5
-rw-r--r--tests/test_Ofast.out19
-rw-r--r--tests/test_S_bfs.out19
-rw-r--r--tests/test_S_ids.out19
-rw-r--r--tests/test_acl.out1
-rw-r--r--tests/test_basic.out19
-rw-r--r--tests/test_capable.out1
-rw-r--r--tests/test_closed_stdin.out19
-rw-r--r--tests/test_color_ls.out12
-rw-r--r--tests/test_color_mi.out20
-rw-r--r--tests/test_color_nul.outbin20 -> 0 bytes
-rw-r--r--tests/test_color_or0_mi.out20
-rw-r--r--tests/test_color_or0_mi0.out20
-rw-r--r--tests/test_color_st0_tw0_ow0.out20
-rw-r--r--tests/test_color_star.out20
-rw-r--r--tests/test_d_path.out19
-rw-r--r--tests/test_data_flow_group.out19
-rw-r--r--tests/test_data_flow_hidden.out19
-rw-r--r--tests/test_data_flow_sparse.out19
-rw-r--r--tests/test_data_flow_user.out19
-rw-r--r--tests/test_daystart.out19
-rw-r--r--tests/test_daystart_twice.out19
-rw-r--r--tests/test_delete.out1
-rw-r--r--tests/test_delete_many.out1
-rw-r--r--tests/test_depth.out19
-rw-r--r--tests/test_depth_error.out2
-rw-r--r--tests/test_depth_overflow.out19
-rw-r--r--tests/test_empty_special.out14
-rw-r--r--tests/test_exec.out19
-rw-r--r--tests/test_exec_plus_status.out19
-rw-r--r--tests/test_flags.out1
-rw-r--r--tests/test_fprint_duplicate_stdout.out38
-rw-r--r--tests/test_fstype.out19
-rw-r--r--tests/test_gid.out19
-rw-r--r--tests/test_gid_minus.out19
-rw-r--r--tests/test_gid_minus_plus.out19
-rw-r--r--tests/test_gid_name.out19
-rw-r--r--tests/test_gid_plus.out19
-rw-r--r--tests/test_gid_plus_plus.out19
-rw-r--r--tests/test_group_id.out19
-rw-r--r--tests/test_group_name.out19
-rw-r--r--tests/test_group_nogroup.out19
-rw-r--r--tests/test_inum_bind_mount.out2
-rw-r--r--tests/test_inum_mount.out1
-rw-r--r--tests/test_mount.out4
-rw-r--r--tests/test_path_d.out19
-rw-r--r--tests/test_printf_Y_error.out3
-rw-r--r--tests/test_printf_nul.outbin8 -> 0 bytes
-rw-r--r--tests/test_rm.out1
-rw-r--r--tests/test_stderr_fails_silently.out19
-rw-r--r--tests/test_true.out19
-rw-r--r--tests/test_type_bind_mount.out1
-rw-r--r--tests/test_uid.out19
-rw-r--r--tests/test_uid_minus.out19
-rw-r--r--tests/test_uid_minus_plus.out19
-rw-r--r--tests/test_uid_name.out19
-rw-r--r--tests/test_uid_plus.out19
-rw-r--r--tests/test_uid_plus_plus.out19
-rw-r--r--tests/test_unique_depth.out19
-rw-r--r--tests/test_user_id.out19
-rw-r--r--tests/test_user_name.out19
-rw-r--r--tests/test_user_nouser.out19
-rw-r--r--tests/test_xattr.out3
-rw-r--r--tests/test_xattrname.out2
-rw-r--r--tests/test_xdev.out4
-rw-r--r--tests/test_xtype_bind_mount.out2
-rw-r--r--tests/tests.h73
-rw-r--r--tests/tests.mk13
-rwxr-xr-xtests/tests.sh20
-rw-r--r--tests/trie.c96
-rw-r--r--tests/util.sh185
-rw-r--r--tests/xspawn.c206
-rw-r--r--tests/xspawnee.c17
-rw-r--r--tests/xtime.c191
-rw-r--r--tests/xtimegm.c107
-rw-r--r--tests/xtouch.c282
960 files changed, 5760 insertions, 1551 deletions
diff --git a/tests/alloc.c b/tests/alloc.c
new file mode 100644
index 0000000..6c0defd
--- /dev/null
+++ b/tests/alloc.c
@@ -0,0 +1,52 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "alloc.h"
+#include "diag.h"
+#include <errno.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+bool check_alloc(void) {
+ bool ret = true;
+
+ // Check sizeof_flex()
+ struct flexible {
+ alignas(64) int foo[8];
+ int bar[];
+ };
+ ret &= bfs_check(sizeof_flex(struct flexible, bar, 0) >= sizeof(struct flexible));
+ ret &= bfs_check(sizeof_flex(struct flexible, bar, 16) % alignof(struct flexible) == 0);
+
+ size_t too_many = SIZE_MAX / sizeof(int) + 1;
+ ret &= bfs_check(sizeof_flex(struct flexible, bar, too_many) == align_floor(alignof(struct flexible), SIZE_MAX));
+
+ // Corner case: sizeof(type) > align_ceil(alignof(type), offsetof(type, member))
+ // Doesn't happen in typical ABIs
+ ret &= bfs_check(flex_size(8, 16, 4, 4, 1) == 16);
+
+ // Make sure we detect allocation size overflows
+#if __GNUC__ && !__clang__
+# pragma GCC diagnostic ignored "-Walloc-size-larger-than="
+#endif
+
+ ret &= bfs_check(ALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW);
+ ret &= bfs_check(ZALLOC_ARRAY(int, too_many) == NULL && errno == EOVERFLOW);
+ ret &= bfs_check(ALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW);
+ ret &= bfs_check(ZALLOC_FLEX(struct flexible, bar, too_many) == NULL && errno == EOVERFLOW);
+
+ // varena tests
+ struct varena varena;
+ VARENA_INIT(&varena, struct flexible, bar);
+
+ for (size_t i = 0; i < 256; ++i) {
+ bfs_verify(varena_alloc(&varena, i));
+ struct arena *arena = &varena.arenas[varena.narenas - 1];
+ ret &= bfs_check(arena->size >= sizeof_flex(struct flexible, bar, i));
+ }
+
+ varena_destroy(&varena);
+ return ret;
+}
diff --git a/tests/test_S_dfs.out b/tests/bfs/D_all.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_S_dfs.out
+++ b/tests/bfs/D_all.out
diff --git a/tests/bfs/D_all.sh b/tests/bfs/D_all.sh
new file mode 100644
index 0000000..170698a
--- /dev/null
+++ b/tests/bfs/D_all.sh
@@ -0,0 +1 @@
+bfs_diff -D all basic
diff --git a/tests/bfs/D_incomplete.sh b/tests/bfs/D_incomplete.sh
new file mode 100644
index 0000000..30c522a
--- /dev/null
+++ b/tests/bfs/D_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -D
diff --git a/tests/test_fprint.out b/tests/bfs/D_multi.out
index a7ccfe4..a7ccfe4 100644
--- a/tests/test_fprint.out
+++ b/tests/bfs/D_multi.out
diff --git a/tests/bfs/D_multi.sh b/tests/bfs/D_multi.sh
new file mode 100644
index 0000000..08a8ca6
--- /dev/null
+++ b/tests/bfs/D_multi.sh
@@ -0,0 +1 @@
+bfs_diff -D opt,tree,unknown basic
diff --git a/tests/test_type_f.out b/tests/bfs/D_opt.out
index 6218a0c..6218a0c 100644
--- a/tests/test_type_f.out
+++ b/tests/bfs/D_opt.out
diff --git a/tests/bfs/D_opt.sh b/tests/bfs/D_opt.sh
new file mode 100644
index 0000000..c14fe70
--- /dev/null
+++ b/tests/bfs/D_opt.sh
@@ -0,0 +1 @@
+bfs_diff -D opt -nohidden -not \( -type c -o -type d \) -links -5 -links -10 -not -hidden basic
diff --git a/tests/test_O0.out b/tests/bfs/D_unknown.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_O0.out
+++ b/tests/bfs/D_unknown.out
@@ -2,18 +2,18 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/bfs/D_unknown.sh b/tests/bfs/D_unknown.sh
new file mode 100644
index 0000000..cac9bd9
--- /dev/null
+++ b/tests/bfs/D_unknown.sh
@@ -0,0 +1,4 @@
+stderr=$(invoke_bfs -warn -D unknown basic 2>&1 >"$OUT")
+[ -n "$stderr" ]
+sort_output
+diff_output
diff --git a/tests/bfs/L_capable.out b/tests/bfs/L_capable.out
new file mode 100644
index 0000000..0810d4a
--- /dev/null
+++ b/tests/bfs/L_capable.out
@@ -0,0 +1,2 @@
+./capable
+./link
diff --git a/tests/bfs/L_capable.sh b/tests/bfs/L_capable.sh
new file mode 100644
index 0000000..97c404f
--- /dev/null
+++ b/tests/bfs/L_capable.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+invoke_bfs . -quit -capable || skip
+
+cd "$TEST"
+
+"$XTOUCH" normal capable
+bfs_sudo setcap all+ep capable || skip
+ln -s capable link
+
+bfs_diff -L . -capable
diff --git a/tests/test_L_unique.out b/tests/bfs/L_unique.out
index c94c48e..c94c48e 100644
--- a/tests/test_L_unique.out
+++ b/tests/bfs/L_unique.out
diff --git a/tests/bfs/L_unique.sh b/tests/bfs/L_unique.sh
new file mode 100644
index 0000000..c804526
--- /dev/null
+++ b/tests/bfs/L_unique.sh
@@ -0,0 +1 @@
+bfs_diff -L links/{file,symlink,hardlink} -unique
diff --git a/tests/test_L_unique_depth.out b/tests/bfs/L_unique_depth.out
index dad0a98..dad0a98 100644
--- a/tests/test_L_unique_depth.out
+++ b/tests/bfs/L_unique_depth.out
diff --git a/tests/bfs/L_unique_depth.sh b/tests/bfs/L_unique_depth.sh
new file mode 100644
index 0000000..fb9aca1
--- /dev/null
+++ b/tests/bfs/L_unique_depth.sh
@@ -0,0 +1 @@
+bfs_diff -L loops/deeply/nested -unique -depth
diff --git a/tests/test_L_unique_loops.out b/tests/bfs/L_unique_loops.out
index dad0a98..dad0a98 100644
--- a/tests/test_L_unique_loops.out
+++ b/tests/bfs/L_unique_loops.out
diff --git a/tests/bfs/L_unique_loops.sh b/tests/bfs/L_unique_loops.sh
new file mode 100644
index 0000000..2bdd94e
--- /dev/null
+++ b/tests/bfs/L_unique_loops.sh
@@ -0,0 +1 @@
+bfs_diff -L loops/deeply/nested -unique
diff --git a/tests/test_O1.out b/tests/bfs/O0.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_O1.out
+++ b/tests/bfs/O0.out
@@ -2,18 +2,18 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/bfs/O0.sh b/tests/bfs/O0.sh
new file mode 100644
index 0000000..0f92d71
--- /dev/null
+++ b/tests/bfs/O0.sh
@@ -0,0 +1 @@
+bfs_diff -O0 basic -not \( -type f -not -type f \)
diff --git a/tests/test_O2.out b/tests/bfs/O1.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_O2.out
+++ b/tests/bfs/O1.out
@@ -2,18 +2,18 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/bfs/O1.sh b/tests/bfs/O1.sh
new file mode 100644
index 0000000..924b410
--- /dev/null
+++ b/tests/bfs/O1.sh
@@ -0,0 +1 @@
+bfs_diff -O1 basic -not \( -type f -not -type f \)
diff --git a/tests/test_O3.out b/tests/bfs/O2.out
index bb3cd8d..a7ccfe4 100644
--- a/tests/test_O3.out
+++ b/tests/bfs/O2.out
@@ -2,18 +2,18 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/bfs/O2.sh b/tests/bfs/O2.sh
new file mode 100644
index 0000000..9382456
--- /dev/null
+++ b/tests/bfs/O2.sh
@@ -0,0 +1 @@
+bfs_diff -O2 basic -not \( -type f -not -type f \)
diff --git a/tests/bfs/O3.out b/tests/bfs/O3.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/O3.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/O3.sh b/tests/bfs/O3.sh
new file mode 100644
index 0000000..5bdf2bc
--- /dev/null
+++ b/tests/bfs/O3.sh
@@ -0,0 +1 @@
+bfs_diff -O3 basic -not \( -type f -not -type f \)
diff --git a/tests/bfs/O9.out b/tests/bfs/O9.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/O9.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/O9.sh b/tests/bfs/O9.sh
new file mode 100644
index 0000000..c12a7a3
--- /dev/null
+++ b/tests/bfs/O9.sh
@@ -0,0 +1,4 @@
+stderr=$(invoke_bfs -warn -O9 basic 2>&1 >"$OUT")
+[ -n "$stderr" ]
+sort_output
+diff_output
diff --git a/tests/bfs/Ofast.out b/tests/bfs/Ofast.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/Ofast.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/Ofast.sh b/tests/bfs/Ofast.sh
new file mode 100644
index 0000000..87c1d8d
--- /dev/null
+++ b/tests/bfs/Ofast.sh
@@ -0,0 +1 @@
+bfs_diff -Ofast basic -not \( -xtype f -not -xtype f \)
diff --git a/tests/test_D_all.out b/tests/bfs/S_bfs.out
index bb3cd8d..bb3cd8d 100644
--- a/tests/test_D_all.out
+++ b/tests/bfs/S_bfs.out
diff --git a/tests/bfs/S_bfs.sh b/tests/bfs/S_bfs.sh
new file mode 100644
index 0000000..76976de
--- /dev/null
+++ b/tests/bfs/S_bfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S bfs -s basic >"$OUT"
+diff_output
diff --git a/tests/bfs/S_dfs.out b/tests/bfs/S_dfs.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/S_dfs.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/S_dfs.sh b/tests/bfs/S_dfs.sh
new file mode 100644
index 0000000..7dd7a46
--- /dev/null
+++ b/tests/bfs/S_dfs.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S dfs -s basic >"$OUT"
+diff_output
diff --git a/tests/test_D_multi.out b/tests/bfs/S_ids.out
index bb3cd8d..bb3cd8d 100644
--- a/tests/test_D_multi.out
+++ b/tests/bfs/S_ids.out
diff --git a/tests/bfs/S_ids.sh b/tests/bfs/S_ids.sh
new file mode 100644
index 0000000..3995cf0
--- /dev/null
+++ b/tests/bfs/S_ids.sh
@@ -0,0 +1,2 @@
+invoke_bfs -S ids -s basic >"$OUT"
+diff_output
diff --git a/tests/bfs/and_incomplete.sh b/tests/bfs/and_incomplete.sh
new file mode 100644
index 0000000..05abc2d
--- /dev/null
+++ b/tests/bfs/and_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print -a
diff --git a/tests/bfs/capable.out b/tests/bfs/capable.out
new file mode 100644
index 0000000..ac7b5ce
--- /dev/null
+++ b/tests/bfs/capable.out
@@ -0,0 +1 @@
+./capable
diff --git a/tests/bfs/capable.sh b/tests/bfs/capable.sh
new file mode 100644
index 0000000..35bb0b4
--- /dev/null
+++ b/tests/bfs/capable.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+invoke_bfs . -quit -capable || skip
+
+cd "$TEST"
+
+"$XTOUCH" normal capable
+bfs_sudo setcap all+ep capable || skip
+ln -s capable link
+
+bfs_diff . -capable
diff --git a/tests/bfs/closed_stderr.sh b/tests/bfs/closed_stderr.sh
new file mode 100644
index 0000000..26abd85
--- /dev/null
+++ b/tests/bfs/closed_stderr.sh
@@ -0,0 +1,4 @@
+# Check if the platform automatically re-opens stderr before we can
+(bash -c 'echo >&2' 2>&-) && skip
+
+! invoke_bfs basic >&- 2>&-
diff --git a/tests/bfs/closed_stdin.out b/tests/bfs/closed_stdin.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/closed_stdin.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/closed_stdin.sh b/tests/bfs/closed_stdin.sh
new file mode 100644
index 0000000..6932be8
--- /dev/null
+++ b/tests/bfs/closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic <&-
diff --git a/tests/bfs/closed_stdout.sh b/tests/bfs/closed_stdout.sh
new file mode 100644
index 0000000..5b6f7c3
--- /dev/null
+++ b/tests/bfs/closed_stdout.sh
@@ -0,0 +1,4 @@
+# Check if the platform automatically re-opens stdout before we can
+(bash -c echo >&-) && skip
+
+! invoke_bfs basic >&-
diff --git a/tests/test_color.out b/tests/bfs/color.out
index 77fc8a8..a439814 100644
--- a/tests/test_color.out
+++ b/tests/bfs/color.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color.sh b/tests/bfs/color.sh
new file mode 100644
index 0000000..23f05a3
--- /dev/null
+++ b/tests/bfs/color.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -color
diff --git a/tests/test_color_L.out b/tests/bfs/color_L.out
index b60dd4a..85923db 100644
--- a/tests/test_color_L.out
+++ b/tests/bfs/color_L.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/chardev_link
@@ -13,8 +15,13 @@
rainbow/file.dat
rainbow/file.txt
rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_L.sh b/tests/bfs/color_L.sh
new file mode 100644
index 0000000..823db62
--- /dev/null
+++ b/tests/bfs/color_L.sh
@@ -0,0 +1 @@
+bfs_diff -L rainbow -color
diff --git a/tests/test_color_ln_target.out b/tests/bfs/color_L_ln_target.out
index cd4ec5e..23fe8d7 100644
--- a/tests/test_color_ln_target.out
+++ b/tests/bfs/color_L_ln_target.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/file.dat
rainbow/file.txt
rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_L_ln_target.sh b/tests/bfs/color_L_ln_target.sh
new file mode 100644
index 0000000..cc5991d
--- /dev/null
+++ b/tests/bfs/color_L_ln_target.sh
@@ -0,0 +1 @@
+LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff -L rainbow -color
diff --git a/tests/test_color_L_no_stat.out b/tests/bfs/color_L_no_stat.out
index c0bb1be..72e0319 100644
--- a/tests/test_color_L_no_stat.out
+++ b/tests/bfs/color_L_no_stat.out
@@ -1,8 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/chardev_link
-rainbow/ow
-rainbow/sticky
-rainbow/sticky_ow
rainbow/socket
rainbow/broken
rainbow/file.txt
@@ -10,11 +9,19 @@
rainbow/pipe
rainbow/exec.sh
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/sugid
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
+rainbow/sticky_ow
diff --git a/tests/bfs/color_L_no_stat.sh b/tests/bfs/color_L_no_stat.sh
new file mode 100644
index 0000000..0a2caf0
--- /dev/null
+++ b/tests/bfs/color_L_no_stat.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff -L rainbow -color
diff --git a/tests/test_color_ext_override.out b/tests/bfs/color_auto.out
index 1377b65..a439814 100644
--- a/tests/test_color_ext_override.out
+++ b/tests/bfs/color_auto.out
@@ -1,8 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/star.tar
-rainbow/star.gz
-rainbow/star.tar.gz
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -16,5 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_auto.sh b/tests/bfs/color_auto.sh
new file mode 100644
index 0000000..7e875cc
--- /dev/null
+++ b/tests/bfs/color_auto.sh
@@ -0,0 +1,4 @@
+unset NO_COLOR
+bfs_pty rainbow >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/color_cd0_no.out b/tests/bfs/color_cd0_no.out
new file mode 100644
index 0000000..37b3fbc
--- /dev/null
+++ b/tests/bfs/color_cd0_no.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/file.dat
+rainbow/file.txt
+rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/chardev_link
diff --git a/tests/bfs/color_cd0_no.sh b/tests/bfs/color_cd0_no.sh
new file mode 100644
index 0000000..325a782
--- /dev/null
+++ b/tests/bfs/color_cd0_no.sh
@@ -0,0 +1 @@
+LS_COLORS="ln=target:cd=0:no=01;92:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_deep.out b/tests/bfs/color_deep.out
new file mode 100644
index 0000000..fb990d5
--- /dev/null
+++ b/tests/bfs/color_deep.out
@@ -0,0 +1,16 @@
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
diff --git a/tests/bfs/color_deep.sh b/tests/bfs/color_deep.sh
new file mode 100644
index 0000000..a83ee0e
--- /dev/null
+++ b/tests/bfs/color_deep.sh
@@ -0,0 +1,7 @@
+name="0123456789ABCDEF"
+name="${name}${name}${name}${name}"
+name="${name}${name}${name}${name}"
+name="${name:0:255}"
+export LS_COLORS="*${name}=01:"
+
+bfs_diff deep -color -type f -printf '%f\n'
diff --git a/tests/test_color_escapes.out b/tests/bfs/color_escapes.out
index b71e138..0bf9fbb 100644
--- a/tests/test_color_escapes.out
+++ b/tests/bfs/color_escapes.out
@@ -1,3 +1,5 @@
+:$'rainbow/\e[1m'
+:$'rainbow/\e[1m/'$'\e[0m'
:rainbow
:rainbow/:exec.sh
:rainbow/:socket
@@ -13,8 +15,13 @@
:rainbow/:sticky
:rainbow/file.dat
:rainbow/file.txt
+:rainbow/lower.gz
+:rainbow/lower.tar
+:rainbow/lower.tar.gz
+:rainbow/lu.tar.GZ
:rainbow/mh1
:rainbow/mh2
-:rainbow/star.gz
-:rainbow/star.tar
-:rainbow/star.tar.gz
+:rainbow/ul.TAR.gz
+:rainbow/upper.GZ
+:rainbow/upper.TAR
+:rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_escapes.sh b/tests/bfs/color_escapes.sh
new file mode 100644
index 0000000..eb5817f
--- /dev/null
+++ b/tests/bfs/color_escapes.sh
@@ -0,0 +1 @@
+LS_COLORS="lc=\e[:rc=\155\::ec=^[\x5B\x6d:" bfs_diff rainbow -color
diff --git a/tests/test_color_missing_colon.out b/tests/bfs/color_ext.out
index cf26e73..218100f 100644
--- a/tests/test_color_missing_colon.out
+++ b/tests/bfs/color_ext.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/suid
rainbow/sticky
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_ext.sh b/tests/bfs/color_ext.sh
new file mode 100644
index 0000000..c9f6d46
--- /dev/null
+++ b/tests/bfs/color_ext.sh
@@ -0,0 +1 @@
+LS_COLORS="*.txt=01:" bfs_diff rainbow -color
diff --git a/tests/test_color_ext0.out b/tests/bfs/color_ext0.out
index e764a6b..d2a7fd5 100644
--- a/tests/test_color_ext0.out
+++ b/tests/bfs/color_ext0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/file.txt
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/suid
rainbow/sticky
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_ext0.sh b/tests/bfs/color_ext0.sh
new file mode 100644
index 0000000..371a9c5
--- /dev/null
+++ b/tests/bfs/color_ext0.sh
@@ -0,0 +1 @@
+LS_COLORS="*.txt=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_case.out b/tests/bfs/color_ext_case.out
new file mode 100644
index 0000000..93dc8f6
--- /dev/null
+++ b/tests/bfs/color_ext_case.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.gz
+rainbow/lower.tar.gz
+rainbow/exec.sh
+rainbow/upper.GZ
+rainbow/upper.TAR.GZ
+rainbow/lower.tar
+rainbow/upper.TAR
+rainbow/ul.TAR.gz
+rainbow/lu.tar.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/file.txt
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/mh1
+rainbow/mh2
diff --git a/tests/bfs/color_ext_case.sh b/tests/bfs/color_ext_case.sh
new file mode 100644
index 0000000..4adba69
--- /dev/null
+++ b/tests/bfs/color_ext_case.sh
@@ -0,0 +1,6 @@
+# *.gz=01;31:*.GZ=01;32 -- case sensitive
+# *.tAr=01;33:*.TaR=01;33 -- case-insensitive
+# *.TAR.gz=01;34:*.tar.GZ=01;35 -- case-sensitive
+# *.txt=35:*TXT=36 -- case-insensitive
+export LS_COLORS="*.gz=01;31:*.GZ=01;32:*.tAr=01;33:*.TaR=01;33:*.TAR.gz=01;34:*.tar.GZ=01;35:*.txt=35:*TXT=36"
+bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_override.out b/tests/bfs/color_ext_override.out
new file mode 100644
index 0000000..0acfcbc
--- /dev/null
+++ b/tests/bfs/color_ext_override.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/lower.tar
+rainbow/upper.TAR
+rainbow/lower.gz
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/mh1
+rainbow/mh2
diff --git a/tests/bfs/color_ext_override.sh b/tests/bfs/color_ext_override.sh
new file mode 100644
index 0000000..ac4c7fb
--- /dev/null
+++ b/tests/bfs/color_ext_override.sh
@@ -0,0 +1 @@
+LS_COLORS="*.tar.gz=01;31:*.TAR=01;32:*.gz=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ext_underride.out b/tests/bfs/color_ext_underride.out
new file mode 100644
index 0000000..5c98341
--- /dev/null
+++ b/tests/bfs/color_ext_underride.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.TAR.GZ
+rainbow/exec.sh
+rainbow/lower.tar
+rainbow/upper.TAR
+rainbow/lower.gz
+rainbow/upper.GZ
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/mh1
+rainbow/mh2
diff --git a/tests/bfs/color_ext_underride.sh b/tests/bfs/color_ext_underride.sh
new file mode 100644
index 0000000..fb12e01
--- /dev/null
+++ b/tests/bfs/color_ext_underride.sh
@@ -0,0 +1 @@
+LS_COLORS="*.gz=01;33:*.TAR=01;32:*.tar.gz=01;31:" bfs_diff rainbow -color
diff --git a/tests/test_color_ext_underride.out b/tests/bfs/color_fi0_no.out
index 787248a..a439814 100644
--- a/tests/test_color_ext_underride.out
+++ b/tests/bfs/color_fi0_no.out
@@ -1,8 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
-rainbow/star.tar.gz
rainbow/exec.sh
-rainbow/star.tar
-rainbow/star.gz
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -16,5 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_fi0_no.sh b/tests/bfs/color_fi0_no.sh
new file mode 100644
index 0000000..f947d64
--- /dev/null
+++ b/tests/bfs/color_fi0_no.sh
@@ -0,0 +1 @@
+LS_COLORS="fi=0:no=01;92:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_fi_no.out b/tests/bfs/color_fi_no.out
new file mode 100644
index 0000000..1c1ad8e
--- /dev/null
+++ b/tests/bfs/color_fi_no.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
diff --git a/tests/bfs/color_fi_no.sh b/tests/bfs/color_fi_no.sh
new file mode 100644
index 0000000..c2b4ec7
--- /dev/null
+++ b/tests/bfs/color_fi_no.sh
@@ -0,0 +1 @@
+LS_COLORS="fi=01;91:no=01;92:" bfs_diff rainbow -color
diff --git a/tests/test_color_L_ln_target.out b/tests/bfs/color_ln_target.out
index cd4ec5e..23fe8d7 100644
--- a/tests/test_color_L_ln_target.out
+++ b/tests/bfs/color_ln_target.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/file.dat
rainbow/file.txt
rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_ln_target.sh b/tests/bfs/color_ln_target.sh
new file mode 100644
index 0000000..707d25e
--- /dev/null
+++ b/tests/bfs/color_ln_target.sh
@@ -0,0 +1 @@
+LS_COLORS="ln=target:or=01;31:mi=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_ls.out b/tests/bfs/color_ls.out
new file mode 100644
index 0000000..f69eb9c
--- /dev/null
+++ b/tests/bfs/color_ls.out
@@ -0,0 +1,12 @@
+scratch/foo/bar
+scratch/foo/bar
+/__bfs__/nowhere
+/__bfs__/nowhere
+foo/bar/nowhere
+foo/bar/nowhere
+foo/bar/nowhere/nothing
+foo/bar/nowhere/nothing
+foo/bar/baz
+foo/bar/baz
+foo/bar/baz//qux
+foo/bar/baz//qux
diff --git a/tests/bfs/color_ls.sh b/tests/bfs/color_ls.sh
new file mode 100644
index 0000000..f1cc216
--- /dev/null
+++ b/tests/bfs/color_ls.sh
@@ -0,0 +1,15 @@
+cd "$TEST"
+"$XTOUCH" -p scratch/foo/bar/baz
+ln -s foo/bar/baz scratch/link
+ln -s foo/bar/nowhere scratch/broken
+ln -s foo/bar/nowhere/nothing scratch/nested
+ln -s foo/bar/baz//qux scratch/notdir
+ln -s scratch/foo/bar scratch/relative
+mkdir scratch/__bfs__
+ln -s /__bfs__/nowhere scratch/absolute
+
+export LS_COLORS="or=01;31:"
+invoke_bfs scratch/{,link,broken,nested,notdir,relative,absolute} -color -type l -ls \
+ | sed 's/.* -> //' \
+ | sort >"$OUT"
+diff_output
diff --git a/tests/test_color_mh.out b/tests/bfs/color_mh.out
index 757a6a1..c658082 100644
--- a/tests/test_color_mh.out
+++ b/tests/bfs/color_mh.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -15,6 +17,11 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_mh.sh b/tests/bfs/color_mh.sh
new file mode 100644
index 0000000..aff1845
--- /dev/null
+++ b/tests/bfs/color_mh.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=01:" bfs_diff rainbow -color
diff --git a/tests/test_color_mh0.out b/tests/bfs/color_mh0.out
index 77fc8a8..a439814 100644
--- a/tests/test_color_mh0.out
+++ b/tests/bfs/color_mh0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_mh0.sh b/tests/bfs/color_mh0.sh
new file mode 100644
index 0000000..7de880d
--- /dev/null
+++ b/tests/bfs/color_mh0.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_mi.out b/tests/bfs/color_mi.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/color_mi.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_mi.sh b/tests/bfs/color_mi.sh
new file mode 100644
index 0000000..06dd8c6
--- /dev/null
+++ b/tests/bfs/color_mi.sh
@@ -0,0 +1 @@
+LS_COLORS="mi=01:" bfs_diff rainbow -color
diff --git a/tests/test_color_ext.out b/tests/bfs/color_missing_colon.out
index cf26e73..218100f 100644
--- a/tests/test_color_ext.out
+++ b/tests/bfs/color_missing_colon.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/suid
rainbow/sticky
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_missing_colon.sh b/tests/bfs/color_missing_colon.sh
new file mode 100644
index 0000000..afa3763
--- /dev/null
+++ b/tests/bfs/color_missing_colon.sh
@@ -0,0 +1 @@
+LS_COLORS="*.txt=01" bfs_diff rainbow -color
diff --git a/tests/bfs/color_no.out b/tests/bfs/color_no.out
new file mode 100644
index 0000000..67e1eee
--- /dev/null
+++ b/tests/bfs/color_no.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
diff --git a/tests/bfs/color_no.sh b/tests/bfs/color_no.sh
new file mode 100644
index 0000000..b7527cb
--- /dev/null
+++ b/tests/bfs/color_no.sh
@@ -0,0 +1 @@
+LS_COLORS="no=01;92:" bfs_diff rainbow -color
diff --git a/tests/test_color_no_stat.out b/tests/bfs/color_no_stat.out
index 1fc5324..e3031b2 100644
--- a/tests/test_color_no_stat.out
+++ b/tests/bfs/color_no_stat.out
@@ -1,7 +1,6 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
-rainbow/ow
-rainbow/sticky
-rainbow/sticky_ow
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -10,11 +9,19 @@
rainbow/pipe
rainbow/exec.sh
rainbow/file.dat
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/sugid
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
+rainbow/sticky_ow
diff --git a/tests/bfs/color_no_stat.sh b/tests/bfs/color_no_stat.sh
new file mode 100644
index 0000000..0bc2520
--- /dev/null
+++ b/tests/bfs/color_no_stat.sh
@@ -0,0 +1 @@
+LS_COLORS="mh=0:ex=0:sg=0:su=0:st=0:ow=0:tw=0:*.txt=01:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_nul.out b/tests/bfs/color_nul.out
new file mode 100644
index 0000000..8ccd9a7
--- /dev/null
+++ b/tests/bfs/color_nul.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/lower.gz
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR.GZ
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.tar
+rainbow/mh1
+rainbow/mh2
+rainbow/upper.TAR
diff --git a/tests/bfs/color_nul.sh b/tests/bfs/color_nul.sh
new file mode 100644
index 0000000..cb662d6
--- /dev/null
+++ b/tests/bfs/color_nul.sh
@@ -0,0 +1,3 @@
+LS_COLORS="ec=\33[\0m:*.gz=\0\61;31:" invoke_bfs rainbow -color | tr '\0' '0' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/test_color_or.out b/tests/bfs/color_or.out
index 9e1fe5c..0bd2570 100644
--- a/tests/test_color_or.out
+++ b/tests/bfs/color_or.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or.sh b/tests/bfs/color_or.sh
new file mode 100644
index 0000000..bccb400
--- /dev/null
+++ b/tests/bfs/color_or.sh
@@ -0,0 +1 @@
+LS_COLORS="or=01:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_or0_mi.out b/tests/bfs/color_or0_mi.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/color_or0_mi.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or0_mi.sh b/tests/bfs/color_or0_mi.sh
new file mode 100644
index 0000000..a362cf1
--- /dev/null
+++ b/tests/bfs/color_or0_mi.sh
@@ -0,0 +1 @@
+LS_COLORS="or=00:mi=01;33:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_or0_mi0.out b/tests/bfs/color_or0_mi0.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/color_or0_mi0.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or0_mi0.sh b/tests/bfs/color_or0_mi0.sh
new file mode 100644
index 0000000..d7c00f6
--- /dev/null
+++ b/tests/bfs/color_or0_mi0.sh
@@ -0,0 +1 @@
+LS_COLORS="or=00:mi=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_or_mi.out b/tests/bfs/color_or_mi.out
index 5667f56..fb67e58 100644
--- a/tests/test_color_or_mi.out
+++ b/tests/bfs/color_or_mi.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or_mi.sh b/tests/bfs/color_or_mi.sh
new file mode 100644
index 0000000..467ce6b
--- /dev/null
+++ b/tests/bfs/color_or_mi.sh
@@ -0,0 +1 @@
+LS_COLORS="or=01;31:mi=01;33:" bfs_diff rainbow -color
diff --git a/tests/test_color_or_mi0.out b/tests/bfs/color_or_mi0.out
index 5667f56..fb67e58 100644
--- a/tests/test_color_or_mi0.out
+++ b/tests/bfs/color_or_mi0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/broken
rainbow/exec.sh
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_or_mi0.sh b/tests/bfs/color_or_mi0.sh
new file mode 100644
index 0000000..a9c36bf
--- /dev/null
+++ b/tests/bfs/color_or_mi0.sh
@@ -0,0 +1 @@
+LS_COLORS="or=01;31:mi=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_rs_lc_rc_ec.out b/tests/bfs/color_rs_lc_rc_ec.out
index a71df24..077ef8d 100644
--- a/tests/test_color_rs_lc_rc_ec.out
+++ b/tests/bfs/color_rs_lc_rc_ec.out
@@ -1,4 +1,5 @@
-LC01;34RCrainbowEC
+LC01;34RC$'rainbow/\e[1m'EC
+LC01;34RC$'rainbow/\e[1m/'EC$'\e[0m'
LC01;34RCrainbow/ECLC01;32RCexec.shEC
LC01;34RCrainbow/ECLC01;35RCsocketEC
LC01;34RCrainbow/ECLC01;36RCbrokenEC
@@ -13,8 +14,14 @@ LC01;34RCrainbow/ECLC37;41RCsuidEC
LC01;34RCrainbow/ECLC37;44RCstickyEC
LC01;34RCrainbow/ECfile.dat
LC01;34RCrainbow/ECfile.txt
+LC01;34RCrainbow/EClower.gz
+LC01;34RCrainbow/EClower.tar
+LC01;34RCrainbow/EClower.tar.gz
+LC01;34RCrainbow/EClu.tar.GZ
LC01;34RCrainbow/ECmh1
LC01;34RCrainbow/ECmh2
-LC01;34RCrainbow/ECstar.gz
-LC01;34RCrainbow/ECstar.tar
-LC01;34RCrainbow/ECstar.tar.gz
+LC01;34RCrainbow/ECul.TAR.gz
+LC01;34RCrainbow/ECupper.GZ
+LC01;34RCrainbow/ECupper.TAR
+LC01;34RCrainbow/ECupper.TAR.GZ
+LC01;34RCrainbowEC
diff --git a/tests/bfs/color_rs_lc_rc_ec.sh b/tests/bfs/color_rs_lc_rc_ec.sh
new file mode 100644
index 0000000..467b2da
--- /dev/null
+++ b/tests/bfs/color_rs_lc_rc_ec.sh
@@ -0,0 +1 @@
+LS_COLORS="rs=RS:lc=LC:rc=RC:ec=EC:" bfs_diff rainbow -color
diff --git a/tests/test_color_st0_tw0_ow.out b/tests/bfs/color_st0_tw0_ow.out
index 9a47ef2..a82762b 100644
--- a/tests/test_color_st0_tw0_ow.out
+++ b/tests/bfs/color_st0_tw0_ow.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/sticky
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/suid
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky
diff --git a/tests/bfs/color_st0_tw0_ow.sh b/tests/bfs/color_st0_tw0_ow.sh
new file mode 100644
index 0000000..8e2b8e3
--- /dev/null
+++ b/tests/bfs/color_st0_tw0_ow.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=00:ow=34;42:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_st0_tw0_ow0.out b/tests/bfs/color_st0_tw0_ow0.out
new file mode 100644
index 0000000..041f1d4
--- /dev/null
+++ b/tests/bfs/color_st0_tw0_ow0.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sgid
+rainbow/pipe
+rainbow/sugid
+rainbow/suid
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
+rainbow/sticky_ow
diff --git a/tests/bfs/color_st0_tw0_ow0.sh b/tests/bfs/color_st0_tw0_ow0.sh
new file mode 100644
index 0000000..c5d5fe7
--- /dev/null
+++ b/tests/bfs/color_st0_tw0_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=00:ow=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_st0_tw_ow.out b/tests/bfs/color_st0_tw_ow.out
index 42549a1..4dcb2f2 100644
--- a/tests/test_color_st0_tw_ow.out
+++ b/tests/bfs/color_st0_tw_ow.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/sticky
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/sticky
diff --git a/tests/bfs/color_st0_tw_ow.sh b/tests/bfs/color_st0_tw_ow.sh
new file mode 100644
index 0000000..8fd9605
--- /dev/null
+++ b/tests/bfs/color_st0_tw_ow.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=40;32:ow=34;42:" bfs_diff rainbow -color
diff --git a/tests/test_color_st0_tw_ow0.out b/tests/bfs/color_st0_tw_ow0.out
index 535b8ae..954ce9c 100644
--- a/tests/test_color_st0_tw_ow0.out
+++ b/tests/bfs/color_st0_tw_ow0.out
@@ -1,7 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/ow
-rainbow/sticky
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +13,15 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
+rainbow/sticky
diff --git a/tests/bfs/color_st0_tw_ow0.sh b/tests/bfs/color_st0_tw_ow0.sh
new file mode 100644
index 0000000..68c63dc
--- /dev/null
+++ b/tests/bfs/color_st0_tw_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=00:tw=40;32:ow=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_st_tw0_ow.out b/tests/bfs/color_st_tw0_ow.out
index c9a86f4..a6e9a16 100644
--- a/tests/test_color_st_tw0_ow.out
+++ b/tests/bfs/color_st_tw0_ow.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -13,8 +15,13 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_st_tw0_ow.sh b/tests/bfs/color_st_tw0_ow.sh
new file mode 100644
index 0000000..be16251
--- /dev/null
+++ b/tests/bfs/color_st_tw0_ow.sh
@@ -0,0 +1 @@
+LS_COLORS="st=37;44:tw=00:ow=34;42:" bfs_diff rainbow -color
diff --git a/tests/test_color_st_tw0_ow0.out b/tests/bfs/color_st_tw0_ow0.out
index 2d94f3a..756dafb 100644
--- a/tests/test_color_st_tw0_ow0.out
+++ b/tests/bfs/color_st_tw0_ow0.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/ow
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
diff --git a/tests/bfs/color_st_tw0_ow0.sh b/tests/bfs/color_st_tw0_ow0.sh
new file mode 100644
index 0000000..f869e7c
--- /dev/null
+++ b/tests/bfs/color_st_tw0_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=37;44:tw=00:ow=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_st_tw_ow0.out b/tests/bfs/color_st_tw_ow0.out
index 317ef90..6e4a260 100644
--- a/tests/test_color_st_tw_ow0.out
+++ b/tests/bfs/color_st_tw_ow0.out
@@ -1,6 +1,7 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
-rainbow/ow
rainbow/socket
rainbow/broken
rainbow/chardev_link
@@ -13,8 +14,14 @@
rainbow/sticky_ow
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
+rainbow/ow
diff --git a/tests/bfs/color_st_tw_ow0.sh b/tests/bfs/color_st_tw_ow0.sh
new file mode 100644
index 0000000..99a17a6
--- /dev/null
+++ b/tests/bfs/color_st_tw_ow0.sh
@@ -0,0 +1 @@
+LS_COLORS="st=37;44:tw=40;32:ow=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/color_star.sh b/tests/bfs/color_star.sh
new file mode 100644
index 0000000..6d5312e
--- /dev/null
+++ b/tests/bfs/color_star.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on LS_COLORS="*"
+! LS_COLORS="*" invoke_bfs rainbow -color
diff --git a/tests/test_color_su0_sg.out b/tests/bfs/color_su0_sg.out
index 8b8c8b8..d13b6b6 100644
--- a/tests/test_color_su0_sg.out
+++ b/tests/bfs/color_su0_sg.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -12,9 +14,14 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_su0_sg.sh b/tests/bfs/color_su0_sg.sh
new file mode 100644
index 0000000..f5f57b4
--- /dev/null
+++ b/tests/bfs/color_su0_sg.sh
@@ -0,0 +1 @@
+LS_COLORS="su=00:sg=30;43:" bfs_diff rainbow -color
diff --git a/tests/test_color_su0_sg0.out b/tests/bfs/color_su0_sg0.out
index 0cd5f9a..77fba58 100644
--- a/tests/test_color_su0_sg0.out
+++ b/tests/bfs/color_su0_sg0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -10,11 +12,16 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
rainbow/sugid
rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_su0_sg0.sh b/tests/bfs/color_su0_sg0.sh
new file mode 100644
index 0000000..0198383
--- /dev/null
+++ b/tests/bfs/color_su0_sg0.sh
@@ -0,0 +1 @@
+LS_COLORS="su=00:sg=00:" bfs_diff rainbow -color
diff --git a/tests/test_color_su_sg0.out b/tests/bfs/color_su_sg0.out
index a9e8c5d..8fab046 100644
--- a/tests/test_color_su_sg0.out
+++ b/tests/bfs/color_su_sg0.out
@@ -1,3 +1,5 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
rainbow
rainbow/exec.sh
rainbow/socket
@@ -12,9 +14,14 @@
rainbow/sticky
rainbow/file.dat
rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
rainbow/mh1
rainbow/mh2
rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_su_sg0.sh b/tests/bfs/color_su_sg0.sh
new file mode 100644
index 0000000..8dc6984
--- /dev/null
+++ b/tests/bfs/color_su_sg0.sh
@@ -0,0 +1 @@
+LS_COLORS="su=37;41:sg=00:" bfs_diff rainbow -color
diff --git a/tests/bfs/comma_incomplete.sh b/tests/bfs/comma_incomplete.sh
new file mode 100644
index 0000000..bd60168
--- /dev/null
+++ b/tests/bfs/comma_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print ,
diff --git a/tests/bfs/data_flow_hidden.out b/tests/bfs/data_flow_hidden.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/data_flow_hidden.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/data_flow_hidden.sh b/tests/bfs/data_flow_hidden.sh
new file mode 100644
index 0000000..6afaab2
--- /dev/null
+++ b/tests/bfs/data_flow_hidden.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -hidden -not -hidden \) -o \( -hidden -o -not -hidden \)
diff --git a/tests/test_deep.out b/tests/bfs/deep_strict.out
index c385fce..c385fce 100644
--- a/tests/test_deep.out
+++ b/tests/bfs/deep_strict.out
diff --git a/tests/bfs/deep_strict.sh b/tests/bfs/deep_strict.sh
new file mode 100644
index 0000000..22453c0
--- /dev/null
+++ b/tests/bfs/deep_strict.sh
@@ -0,0 +1,3 @@
+# Not even enough fds to keep the root open
+ulimit -n $((NOPENFD + 4))
+bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/test_not_prune.out b/tests/bfs/exclude_depth.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_not_prune.out
+++ b/tests/bfs/exclude_depth.out
@@ -2,12 +2,12 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
basic/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/bfs/exclude_depth.sh b/tests/bfs/exclude_depth.sh
new file mode 100644
index 0000000..437b4dd
--- /dev/null
+++ b/tests/bfs/exclude_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -exclude -name foo
diff --git a/tests/bfs/exclude_exclude.sh b/tests/bfs/exclude_exclude.sh
new file mode 100644
index 0000000..739342f
--- /dev/null
+++ b/tests/bfs/exclude_exclude.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -exclude -exclude -name foo
diff --git a/tests/test_L_ilname.out b/tests/bfs/exclude_mindepth.out
index e69de29..e69de29 100644
--- a/tests/test_L_ilname.out
+++ b/tests/bfs/exclude_mindepth.out
diff --git a/tests/bfs/exclude_mindepth.sh b/tests/bfs/exclude_mindepth.sh
new file mode 100644
index 0000000..c8f70f9
--- /dev/null
+++ b/tests/bfs/exclude_mindepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 3 -exclude -name foo
diff --git a/tests/test_prune_or_print.out b/tests/bfs/exclude_name.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_prune_or_print.out
+++ b/tests/bfs/exclude_name.out
@@ -2,12 +2,12 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
basic/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/bfs/exclude_name.sh b/tests/bfs/exclude_name.sh
new file mode 100644
index 0000000..7cf9f33
--- /dev/null
+++ b/tests/bfs/exclude_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -exclude -name foo
diff --git a/tests/bfs/exclude_print.sh b/tests/bfs/exclude_print.sh
new file mode 100644
index 0000000..dc89e1d
--- /dev/null
+++ b/tests/bfs/exclude_print.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -exclude -print
diff --git a/tests/test_name_root.out b/tests/bfs/exec_flush_fprint.out
index 511198f..511198f 100644
--- a/tests/test_name_root.out
+++ b/tests/bfs/exec_flush_fprint.out
diff --git a/tests/bfs/exec_flush_fprint.sh b/tests/bfs/exec_flush_fprint.sh
new file mode 100644
index 0000000..a862773
--- /dev/null
+++ b/tests/bfs/exec_flush_fprint.sh
@@ -0,0 +1,2 @@
+# Even non-stdstreams should be flushed
+bfs_diff basic/a -fprint "$OUT.f" -exec cat "$OUT.f" \;
diff --git a/tests/bfs/exec_flush_fprint_fail.sh b/tests/bfs/exec_flush_fprint_fail.sh
new file mode 100644
index 0000000..cd38e41
--- /dev/null
+++ b/tests/bfs/exec_flush_fprint_fail.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic/a -fprint /dev/full -exec true \;
diff --git a/tests/test_execdir_plus.out b/tests/bfs/execdir_plus.out
index 5bbb758..8866a8f 100644
--- a/tests/test_execdir_plus.out
+++ b/tests/bfs/execdir_plus.out
@@ -1,3 +1,4 @@
+./a ./b ./c ./e ./g ./i ./j ./k ./l
./bar
./bar
./basic
@@ -8,4 +9,3 @@
./foo
./foo
./h
-./a ./b ./c ./e ./g ./i ./j ./k ./l
diff --git a/tests/bfs/execdir_plus.sh b/tests/bfs/execdir_plus.sh
new file mode 100644
index 0000000..6f24bdc
--- /dev/null
+++ b/tests/bfs/execdir_plus.sh
@@ -0,0 +1,4 @@
+tree=$(invoke_bfs -D tree 2>&1 -quit)
+[[ "$tree" == *"-S dfs"* ]] && skip
+
+bfs_diff -j1 basic -execdir "$TESTS/sort-args.sh" {} +
diff --git a/tests/bfs/execdir_plus_nonexistent.out b/tests/bfs/execdir_plus_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/execdir_plus_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/execdir_plus_nonexistent.sh b/tests/bfs/execdir_plus_nonexistent.sh
new file mode 100644
index 0000000..ed7ed56
--- /dev/null
+++ b/tests/bfs/execdir_plus_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -execdir "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/test_H_type_l.out b/tests/bfs/expr_flag_path.out
index e67f10b..e67f10b 100644
--- a/tests/test_H_type_l.out
+++ b/tests/bfs/expr_flag_path.out
diff --git a/tests/bfs/expr_flag_path.sh b/tests/bfs/expr_flag_path.sh
new file mode 100644
index 0000000..bb89d92
--- /dev/null
+++ b/tests/bfs/expr_flag_path.sh
@@ -0,0 +1 @@
+bfs_diff -type l -H links/skip
diff --git a/tests/test_expr_flag_path.out b/tests/bfs/expr_path_flag.out
index e67f10b..e67f10b 100644
--- a/tests/test_expr_flag_path.out
+++ b/tests/bfs/expr_path_flag.out
diff --git a/tests/bfs/expr_path_flag.sh b/tests/bfs/expr_path_flag.sh
new file mode 100644
index 0000000..818e5d1
--- /dev/null
+++ b/tests/bfs/expr_path_flag.sh
@@ -0,0 +1 @@
+bfs_diff -type l links/skip -H
diff --git a/tests/test_expr_path_flag.out b/tests/bfs/flag_expr_path.out
index e67f10b..e67f10b 100644
--- a/tests/test_expr_path_flag.out
+++ b/tests/bfs/flag_expr_path.out
diff --git a/tests/bfs/flag_expr_path.sh b/tests/bfs/flag_expr_path.sh
new file mode 100644
index 0000000..a414e10
--- /dev/null
+++ b/tests/bfs/flag_expr_path.sh
@@ -0,0 +1 @@
+bfs_diff -H -type l links/skip
diff --git a/tests/test_fprint_append.out b/tests/bfs/fprint_duplicate_stdout.out
index 6c21751..6c21751 100644
--- a/tests/test_fprint_append.out
+++ b/tests/bfs/fprint_duplicate_stdout.out
diff --git a/tests/bfs/fprint_duplicate_stdout.sh b/tests/bfs/fprint_duplicate_stdout.sh
new file mode 100644
index 0000000..4e95e30
--- /dev/null
+++ b/tests/bfs/fprint_duplicate_stdout.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprint "$OUT" -print >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/fprint_error_stderr.sh b/tests/bfs/fprint_error_stderr.sh
new file mode 100644
index 0000000..2cc4037
--- /dev/null
+++ b/tests/bfs/fprint_error_stderr.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full 2>/dev/full
diff --git a/tests/bfs/fprint_error_stdout.sh b/tests/bfs/fprint_error_stdout.sh
new file mode 100644
index 0000000..42a7b36
--- /dev/null
+++ b/tests/bfs/fprint_error_stdout.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full >/dev/full
diff --git a/tests/bfs/help.sh b/tests/bfs/help.sh
new file mode 100644
index 0000000..5029c7e
--- /dev/null
+++ b/tests/bfs/help.sh
@@ -0,0 +1,4 @@
+! invoke_bfs -help | grep -E '\{...?\}' || fail
+! invoke_bfs -D help | grep -E '\{...?\}' || fail
+! invoke_bfs -S help | grep -E '\{...?\}' || fail
+! invoke_bfs -regextype help | grep -E '\{...?\}' || fail
diff --git a/tests/test_hidden.out b/tests/bfs/hidden.out
index e65ede9..e65ede9 100644
--- a/tests/test_hidden.out
+++ b/tests/bfs/hidden.out
diff --git a/tests/bfs/hidden.sh b/tests/bfs/hidden.sh
new file mode 100644
index 0000000..b0413c5
--- /dev/null
+++ b/tests/bfs/hidden.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -hidden
diff --git a/tests/test_hidden_root.out b/tests/bfs/hidden_root.out
index 199ae5f..8c1371b 100644
--- a/tests/test_hidden_root.out
+++ b/tests/bfs/hidden_root.out
@@ -1,5 +1,5 @@
...
+.../../...
./...
./...
-.../../...
././...
diff --git a/tests/bfs/hidden_root.sh b/tests/bfs/hidden_root.sh
new file mode 100644
index 0000000..905c5b5
--- /dev/null
+++ b/tests/bfs/hidden_root.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff . ./. ... ./... .../.. -hidden
diff --git a/tests/bfs/high_byte.sh b/tests/bfs/high_byte.sh
new file mode 100644
index 0000000..c76199f
--- /dev/null
+++ b/tests/bfs/high_byte.sh
@@ -0,0 +1 @@
+! invoke_bfs -$'\xFF'
diff --git a/tests/bfs/j0.sh b/tests/bfs/j0.sh
new file mode 100644
index 0000000..97a7c5c
--- /dev/null
+++ b/tests/bfs/j0.sh
@@ -0,0 +1 @@
+! invoke_bfs -j0 basic
diff --git a/tests/bfs/j1.out b/tests/bfs/j1.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/j1.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/j1.sh b/tests/bfs/j1.sh
new file mode 100644
index 0000000..972ac1b
--- /dev/null
+++ b/tests/bfs/j1.sh
@@ -0,0 +1 @@
+bfs_diff -j1 basic
diff --git a/tests/bfs/j64.out b/tests/bfs/j64.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/j64.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/j64.sh b/tests/bfs/j64.sh
new file mode 100644
index 0000000..c56788f
--- /dev/null
+++ b/tests/bfs/j64.sh
@@ -0,0 +1 @@
+bfs_diff -j64 basic
diff --git a/tests/bfs/j_negative.sh b/tests/bfs/j_negative.sh
new file mode 100644
index 0000000..809c98c
--- /dev/null
+++ b/tests/bfs/j_negative.sh
@@ -0,0 +1 @@
+! invoke_bfs -j-1 basic
diff --git a/tests/bfs/limit.out b/tests/bfs/limit.out
new file mode 100644
index 0000000..ea94276
--- /dev/null
+++ b/tests/bfs/limit.out
@@ -0,0 +1,4 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
diff --git a/tests/bfs/limit.sh b/tests/bfs/limit.sh
new file mode 100644
index 0000000..84b605f
--- /dev/null
+++ b/tests/bfs/limit.sh
@@ -0,0 +1 @@
+bfs_diff -s basic -type f -print -limit 4
diff --git a/tests/bfs/limit_0.sh b/tests/bfs/limit_0.sh
new file mode 100644
index 0000000..3ce26de
--- /dev/null
+++ b/tests/bfs/limit_0.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit 0
diff --git a/tests/bfs/limit_implicit_print.sh b/tests/bfs/limit_implicit_print.sh
new file mode 100644
index 0000000..cdb059d
--- /dev/null
+++ b/tests/bfs/limit_implicit_print.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -type f -limit 1
diff --git a/tests/bfs/limit_incomplete.sh b/tests/bfs/limit_incomplete.sh
new file mode 100644
index 0000000..2d1e842
--- /dev/null
+++ b/tests/bfs/limit_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit
diff --git a/tests/bfs/limit_one.sh b/tests/bfs/limit_one.sh
new file mode 100644
index 0000000..3f8181c
--- /dev/null
+++ b/tests/bfs/limit_one.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print -limit one
diff --git a/tests/bfs/links_empty.sh b/tests/bfs/links_empty.sh
new file mode 100644
index 0000000..42cf6e5
--- /dev/null
+++ b/tests/bfs/links_empty.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ''
diff --git a/tests/bfs/links_invalid.sh b/tests/bfs/links_invalid.sh
new file mode 100644
index 0000000..4d139c9
--- /dev/null
+++ b/tests/bfs/links_invalid.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ASDF
diff --git a/tests/bfs/links_leading_space.sh b/tests/bfs/links_leading_space.sh
new file mode 100644
index 0000000..15957af
--- /dev/null
+++ b/tests/bfs/links_leading_space.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links ' 1'
diff --git a/tests/bfs/links_negative.sh b/tests/bfs/links_negative.sh
new file mode 100644
index 0000000..e664b99
--- /dev/null
+++ b/tests/bfs/links_negative.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links +-1
diff --git a/tests/bfs/links_noarg.sh b/tests/bfs/links_noarg.sh
new file mode 100644
index 0000000..5c948dc
--- /dev/null
+++ b/tests/bfs/links_noarg.sh
@@ -0,0 +1 @@
+! invoke_bfs links -links
diff --git a/tests/bfs/newerma_nonexistent.sh b/tests/bfs/newerma_nonexistent.sh
new file mode 100644
index 0000000..cdedb4a
--- /dev/null
+++ b/tests/bfs/newerma_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newerma basic/nonexistent
diff --git a/tests/bfs/newermq.sh b/tests/bfs/newermq.sh
new file mode 100644
index 0000000..2f705dc
--- /dev/null
+++ b/tests/bfs/newermq.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newermq times/a
diff --git a/tests/bfs/newermt_invalid.sh b/tests/bfs/newermt_invalid.sh
new file mode 100644
index 0000000..98efece
--- /dev/null
+++ b/tests/bfs/newermt_invalid.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newermt not_a_date_time
diff --git a/tests/bfs/newerqm.sh b/tests/bfs/newerqm.sh
new file mode 100644
index 0000000..c0cff98
--- /dev/null
+++ b/tests/bfs/newerqm.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newerqm times/a
diff --git a/tests/bfs/nocolor.out b/tests/bfs/nocolor.out
new file mode 100644
index 0000000..d51d24d
--- /dev/null
+++ b/tests/bfs/nocolor.out
@@ -0,0 +1,27 @@
+rainbow
+rainbow/
+rainbow//
+rainbow/broken
+rainbow/chardev_link
+rainbow/exec.sh
+rainbow/file.dat
+rainbow/file.txt
+rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ow
+rainbow/pipe
+rainbow/sgid
+rainbow/socket
+rainbow/sticky
+rainbow/sticky_ow
+rainbow/sugid
+rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/nocolor.sh b/tests/bfs/nocolor.sh
new file mode 100644
index 0000000..8dace0b
--- /dev/null
+++ b/tests/bfs/nocolor.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -nocolor
diff --git a/tests/bfs/nocolor_env.out b/tests/bfs/nocolor_env.out
new file mode 100644
index 0000000..d51d24d
--- /dev/null
+++ b/tests/bfs/nocolor_env.out
@@ -0,0 +1,27 @@
+rainbow
+rainbow/
+rainbow//
+rainbow/broken
+rainbow/chardev_link
+rainbow/exec.sh
+rainbow/file.dat
+rainbow/file.txt
+rainbow/link.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ow
+rainbow/pipe
+rainbow/sgid
+rainbow/socket
+rainbow/sticky
+rainbow/sticky_ow
+rainbow/sugid
+rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/nocolor_env.sh b/tests/bfs/nocolor_env.sh
new file mode 100644
index 0000000..d1c2afb
--- /dev/null
+++ b/tests/bfs/nocolor_env.sh
@@ -0,0 +1,3 @@
+NO_COLOR=1 bfs_pty rainbow >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bfs/nocolor_env_empty.out b/tests/bfs/nocolor_env_empty.out
new file mode 100644
index 0000000..a439814
--- /dev/null
+++ b/tests/bfs/nocolor_env_empty.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/exec.sh
+rainbow/socket
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sticky_ow
+rainbow/sgid
+rainbow/pipe
+rainbow/ow
+rainbow/sugid
+rainbow/suid
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/nocolor_env_empty.sh b/tests/bfs/nocolor_env_empty.sh
new file mode 100644
index 0000000..1edfb1d
--- /dev/null
+++ b/tests/bfs/nocolor_env_empty.sh
@@ -0,0 +1,3 @@
+NO_COLOR= bfs_pty rainbow >"$OUT"
+sort_output
+diff_output
diff --git a/tests/test_nohidden.out b/tests/bfs/nohidden.out
index d2a9690..d3ec901 100644
--- a/tests/test_nohidden.out
+++ b/tests/bfs/nohidden.out
@@ -1,19 +1,21 @@
weirdnames
weirdnames/
+weirdnames/ /j
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/\
-weirdnames/ /j
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/[
+weirdnames/[/k
+weirdnames/\
weirdnames/\/i
diff --git a/tests/bfs/nohidden.sh b/tests/bfs/nohidden.sh
new file mode 100644
index 0000000..e3a3e4a
--- /dev/null
+++ b/tests/bfs/nohidden.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -nohidden
diff --git a/tests/test_nohidden_depth.out b/tests/bfs/nohidden_depth.out
index d2a9690..d3ec901 100644
--- a/tests/test_nohidden_depth.out
+++ b/tests/bfs/nohidden_depth.out
@@ -1,19 +1,21 @@
weirdnames
weirdnames/
+weirdnames/ /j
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/\
-weirdnames/ /j
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/[
+weirdnames/[/k
+weirdnames/\
weirdnames/\/i
diff --git a/tests/bfs/nohidden_depth.sh b/tests/bfs/nohidden_depth.sh
new file mode 100644
index 0000000..9fd7017
--- /dev/null
+++ b/tests/bfs/nohidden_depth.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -depth -nohidden
diff --git a/tests/bfs/nowarn.sh b/tests/bfs/nowarn.sh
new file mode 100644
index 0000000..d9f9ab3
--- /dev/null
+++ b/tests/bfs/nowarn.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs basic -nowarn -depth -prune 2>&1 >/dev/null)
+[ -z "$stderr" ]
diff --git a/tests/test_ok_plus_semicolon.out b/tests/bfs/ok_plus_semicolon.out
index 85d27e5..2a3e14f 100644
--- a/tests/test_ok_plus_semicolon.out
+++ b/tests/bfs/ok_plus_semicolon.out
@@ -2,18 +2,18 @@ basic +
basic/a +
basic/b +
basic/c +
+basic/c/d +
basic/e +
+basic/e/f +
basic/g +
+basic/g/h +
basic/i +
basic/j +
-basic/k +
-basic/l +
-basic/c/d +
-basic/e/f +
-basic/g/h +
basic/j/foo +
+basic/k +
basic/k/foo +
-basic/l/foo +
basic/k/foo/bar +
+basic/l +
+basic/l/foo +
basic/l/foo/bar +
basic/l/foo/bar/baz +
diff --git a/tests/bfs/ok_plus_semicolon.sh b/tests/bfs/ok_plus_semicolon.sh
new file mode 100644
index 0000000..57d6103
--- /dev/null
+++ b/tests/bfs/ok_plus_semicolon.sh
@@ -0,0 +1,8 @@
+# The -ok primary shall be equivalent to -exec, except that the use of a
+# <plus-sign> to punctuate the end of the primary expression need not be
+# supported, ...
+#
+# bfs chooses not to support it, for compatibility with most other find
+# implementations.
+
+yes | bfs_diff basic -ok echo {} + \;
diff --git a/tests/test_okdir_plus_semicolon.out b/tests/bfs/okdir_plus_semicolon.out
index 1909d27..1909d27 100644
--- a/tests/test_okdir_plus_semicolon.out
+++ b/tests/bfs/okdir_plus_semicolon.out
diff --git a/tests/bfs/okdir_plus_semicolon.sh b/tests/bfs/okdir_plus_semicolon.sh
new file mode 100644
index 0000000..d316bd7
--- /dev/null
+++ b/tests/bfs/okdir_plus_semicolon.sh
@@ -0,0 +1 @@
+yes | bfs_diff basic -okdir echo {} + \;
diff --git a/tests/bfs/or_incomplete.sh b/tests/bfs/or_incomplete.sh
new file mode 100644
index 0000000..4af31b6
--- /dev/null
+++ b/tests/bfs/or_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -print -o
diff --git a/tests/test_flag_expr_path.out b/tests/bfs/path_expr_flag.out
index e67f10b..e67f10b 100644
--- a/tests/test_flag_expr_path.out
+++ b/tests/bfs/path_expr_flag.out
diff --git a/tests/bfs/path_expr_flag.sh b/tests/bfs/path_expr_flag.sh
new file mode 100644
index 0000000..7cfa1cd
--- /dev/null
+++ b/tests/bfs/path_expr_flag.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -type l -H
diff --git a/tests/test_path_expr_flag.out b/tests/bfs/path_flag_expr.out
index e67f10b..e67f10b 100644
--- a/tests/test_path_expr_flag.out
+++ b/tests/bfs/path_flag_expr.out
diff --git a/tests/bfs/path_flag_expr.sh b/tests/bfs/path_flag_expr.sh
new file mode 100644
index 0000000..ca00c8c
--- /dev/null
+++ b/tests/bfs/path_flag_expr.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -H -type l
diff --git a/tests/test_L_lname.out b/tests/bfs/perm_leading_plus_symbolic.out
index e69de29..e69de29 100644
--- a/tests/test_L_lname.out
+++ b/tests/bfs/perm_leading_plus_symbolic.out
diff --git a/tests/bfs/perm_leading_plus_symbolic.sh b/tests/bfs/perm_leading_plus_symbolic.sh
new file mode 100644
index 0000000..4202ac1
--- /dev/null
+++ b/tests/bfs/perm_leading_plus_symbolic.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +rwx
diff --git a/tests/bfs/perm_symbolic_double_comma.sh b/tests/bfs/perm_symbolic_double_comma.sh
new file mode 100644
index 0000000..48f9d4b
--- /dev/null
+++ b/tests/bfs/perm_symbolic_double_comma.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a+r,,u+w
diff --git a/tests/bfs/perm_symbolic_missing_action.sh b/tests/bfs/perm_symbolic_missing_action.sh
new file mode 100644
index 0000000..28446ab
--- /dev/null
+++ b/tests/bfs/perm_symbolic_missing_action.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a
diff --git a/tests/bfs/perm_symbolic_trailing_comma.sh b/tests/bfs/perm_symbolic_trailing_comma.sh
new file mode 100644
index 0000000..01bbc16
--- /dev/null
+++ b/tests/bfs/perm_symbolic_trailing_comma.sh
@@ -0,0 +1 @@
+! invoke_bfs perms -perm a+r,
diff --git a/tests/test_printf_color.out b/tests/bfs/printf_color.out
index dc04d14..77d21c3 100644
--- a/tests/test_printf_color.out
+++ b/tests/bfs/printf_color.out
@@ -1,7 +1,11 @@
-. . rainbow ./rainbow rainbow
+. $'./rainbow/\e[1m' $'\e[0m' $'./rainbow/\e[1m/'$'\e[0m' $'rainbow/\e[1m/'$'\e[0m'
+. . . .
+. . rainbow ./rainbow rainbow
. ./rainbow exec.sh ./rainbow/exec.sh rainbow/exec.sh
+. ./rainbow $'\e[1m' $'./rainbow/\e[1m' $'rainbow/\e[1m'
. ./rainbow socket ./rainbow/socket rainbow/socket
. ./rainbow broken ./rainbow/broken rainbow/broken nowhere
+. ./rainbow chardev_link ./rainbow/chardev_link rainbow/chardev_link /dev/null
. ./rainbow link.txt ./rainbow/link.txt rainbow/link.txt file.txt
. ./rainbow sticky_ow ./rainbow/sticky_ow rainbow/sticky_ow
. ./rainbow sgid ./rainbow/sgid rainbow/sgid
@@ -12,9 +16,13 @@
. ./rainbow sticky ./rainbow/sticky rainbow/sticky
. ./rainbow file.dat ./rainbow/file.dat rainbow/file.dat
. ./rainbow file.txt ./rainbow/file.txt rainbow/file.txt
+. ./rainbow lower.gz ./rainbow/lower.gz rainbow/lower.gz
+. ./rainbow lower.tar ./rainbow/lower.tar rainbow/lower.tar
+. ./rainbow lower.tar.gz ./rainbow/lower.tar.gz rainbow/lower.tar.gz
+. ./rainbow lu.tar.GZ ./rainbow/lu.tar.GZ rainbow/lu.tar.GZ
. ./rainbow mh1 ./rainbow/mh1 rainbow/mh1
. ./rainbow mh2 ./rainbow/mh2 rainbow/mh2
-. ./rainbow star.gz ./rainbow/star.gz rainbow/star.gz
-. ./rainbow star.tar ./rainbow/star.tar rainbow/star.tar
-. ./rainbow star.tar.gz ./rainbow/star.tar.gz rainbow/star.tar.gz
-. ./rainbow chardev_link ./rainbow/chardev_link rainbow/chardev_link /dev/null
+. ./rainbow ul.TAR.gz ./rainbow/ul.TAR.gz rainbow/ul.TAR.gz
+. ./rainbow upper.GZ ./rainbow/upper.GZ rainbow/upper.GZ
+. ./rainbow upper.TAR ./rainbow/upper.TAR rainbow/upper.TAR
+. ./rainbow upper.TAR.GZ ./rainbow/upper.TAR.GZ rainbow/upper.TAR.GZ
diff --git a/tests/bfs/printf_color.sh b/tests/bfs/printf_color.sh
new file mode 100644
index 0000000..3641ddb
--- /dev/null
+++ b/tests/bfs/printf_color.sh
@@ -0,0 +1 @@
+bfs_diff -color -exclude \( -depth 1 -not -name rainbow \) -printf '%H %h %f %p %P %l\n'
diff --git a/tests/bfs/printf_duplicate_flag.sh b/tests/bfs/printf_duplicate_flag.sh
new file mode 100644
index 0000000..5ff29f1
--- /dev/null
+++ b/tests/bfs/printf_duplicate_flag.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%--p'
diff --git a/tests/bfs/printf_everything.sh b/tests/bfs/printf_everything.sh
new file mode 100644
index 0000000..07d574a
--- /dev/null
+++ b/tests/bfs/printf_everything.sh
@@ -0,0 +1,15 @@
+everything=(%{a,b,c,d,D,f,g,G,h,H,i,k,l,m,M,n,p,P,s,S,t,u,U,y,Y})
+
+# Check if we have fstypes
+if invoke_bfs basic -printf '%F' -quit >/dev/null; then
+ everything+=(%F)
+fi
+
+everything+=(%{A,C,T}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z})
+
+# Check if we have birth times
+if invoke_bfs basic -printf '%w' -quit >/dev/null; then
+ everything+=(%w %{B,W}{%,+,@,a,A,b,B,c,C,d,D,e,F,g,G,h,H,I,j,k,l,m,M,n,p,r,R,s,S,t,T,u,U,V,w,W,x,X,y,Y,z,Z})
+fi
+
+invoke_bfs rainbow -printf "${everything[*]}\n" >/dev/null
diff --git a/tests/bfs/printf_incomplete_escape.sh b/tests/bfs/printf_incomplete_escape.sh
new file mode 100644
index 0000000..f560d28
--- /dev/null
+++ b/tests/bfs/printf_incomplete_escape.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '\'
diff --git a/tests/bfs/printf_incomplete_format.sh b/tests/bfs/printf_incomplete_format.sh
new file mode 100644
index 0000000..92c6afc
--- /dev/null
+++ b/tests/bfs/printf_incomplete_format.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%'
diff --git a/tests/bfs/printf_invalid_escape.sh b/tests/bfs/printf_invalid_escape.sh
new file mode 100644
index 0000000..4338f9b
--- /dev/null
+++ b/tests/bfs/printf_invalid_escape.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '\!'
diff --git a/tests/bfs/printf_invalid_format.sh b/tests/bfs/printf_invalid_format.sh
new file mode 100644
index 0000000..59d63a7
--- /dev/null
+++ b/tests/bfs/printf_invalid_format.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%!'
diff --git a/tests/bfs/printf_must_be_numeric.sh b/tests/bfs/printf_must_be_numeric.sh
new file mode 100644
index 0000000..7c7c3fa
--- /dev/null
+++ b/tests/bfs/printf_must_be_numeric.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -printf '%+p'
diff --git a/tests/test_and_purity.out b/tests/bfs/printf_w.out
index e69de29..e69de29 100644
--- a/tests/test_and_purity.out
+++ b/tests/bfs/printf_w.out
diff --git a/tests/bfs/printf_w.sh b/tests/bfs/printf_w.sh
new file mode 100644
index 0000000..3b27ee7
--- /dev/null
+++ b/tests/bfs/printf_w.sh
@@ -0,0 +1,2 @@
+# Birth times may not be supported, so just check that %w/%W/%B can be parsed
+bfs_diff times -false -printf '%w %WY %BY\n'
diff --git a/tests/bfs/status.sh b/tests/bfs/status.sh
new file mode 100644
index 0000000..83e12d3
--- /dev/null
+++ b/tests/bfs/status.sh
@@ -0,0 +1 @@
+bfs_pty basic -status -print -depth 0 -exec stty cols 123 rows 14 \; >"$OUT"
diff --git a/tests/bfs/stderr_fails_loudly.sh b/tests/bfs/stderr_fails_loudly.sh
new file mode 100644
index 0000000..8572d5a
--- /dev/null
+++ b/tests/bfs/stderr_fails_loudly.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs -D all basic -false -fprint /dev/full 2>/dev/full
diff --git a/tests/bfs/stderr_fails_silently.out b/tests/bfs/stderr_fails_silently.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/stderr_fails_silently.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/stderr_fails_silently.sh b/tests/bfs/stderr_fails_silently.sh
new file mode 100644
index 0000000..a37393d
--- /dev/null
+++ b/tests/bfs/stderr_fails_silently.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+bfs_diff -D all basic 2>/dev/full
diff --git a/tests/test_type_multi.out b/tests/bfs/type_multi.out
index d22ed9f..3cae08a 100644
--- a/tests/test_type_multi.out
+++ b/tests/bfs/type_multi.out
@@ -1,7 +1,7 @@
links
links/deeply
-links/file
-links/hardlink
links/deeply/nested
links/deeply/nested/dir
links/deeply/nested/file
+links/file
+links/hardlink
diff --git a/tests/bfs/type_multi.sh b/tests/bfs/type_multi.sh
new file mode 100644
index 0000000..59992c7
--- /dev/null
+++ b/tests/bfs/type_multi.sh
@@ -0,0 +1 @@
+bfs_diff links -type f,d,c
diff --git a/tests/bfs/typo.sh b/tests/bfs/typo.sh
new file mode 100644
index 0000000..459e9fe
--- /dev/null
+++ b/tests/bfs/typo.sh
@@ -0,0 +1 @@
+invoke_bfs -dikkiq 2>&1 | grep follow >/dev/null
diff --git a/tests/bfs/unexpected_operator.sh b/tests/bfs/unexpected_operator.sh
new file mode 100644
index 0000000..2eb0e71
--- /dev/null
+++ b/tests/bfs/unexpected_operator.sh
@@ -0,0 +1 @@
+! invoke_bfs \! -o -print
diff --git a/tests/test_unique.out b/tests/bfs/unique.out
index 289cbde..289cbde 100644
--- a/tests/test_unique.out
+++ b/tests/bfs/unique.out
diff --git a/tests/bfs/unique.sh b/tests/bfs/unique.sh
new file mode 100644
index 0000000..ea8adfd
--- /dev/null
+++ b/tests/bfs/unique.sh
@@ -0,0 +1 @@
+bfs_diff links/{file,symlink,hardlink} -unique
diff --git a/tests/bfs/unique_depth.out b/tests/bfs/unique_depth.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/unique_depth.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/unique_depth.sh b/tests/bfs/unique_depth.sh
new file mode 100644
index 0000000..c1d9716
--- /dev/null
+++ b/tests/bfs/unique_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -unique -depth
diff --git a/tests/bfs/version.sh b/tests/bfs/version.sh
new file mode 100644
index 0000000..e0417ca
--- /dev/null
+++ b/tests/bfs/version.sh
@@ -0,0 +1 @@
+invoke_bfs -version >/dev/null
diff --git a/tests/bfs/warn_O9.out b/tests/bfs/warn_O9.out
new file mode 100644
index 0000000..336a6e8
--- /dev/null
+++ b/tests/bfs/warn_O9.out
@@ -0,0 +1,19 @@
+.
+./a
+./b
+./c
+./c/d
+./e
+./e/f
+./g
+./g/h
+./i
+./j
+./j/foo
+./k
+./k/foo
+./k/foo/bar
+./l
+./l/foo
+./l/foo/bar
+./l/foo/bar/baz
diff --git a/tests/bfs/warn_O9.sh b/tests/bfs/warn_O9.sh
new file mode 100644
index 0000000..821789f
--- /dev/null
+++ b/tests/bfs/warn_O9.sh
@@ -0,0 +1,3 @@
+# Regression test: don't crash when warning if -O9 is the last argument
+cd basic
+bfs_diff -warn -O9
diff --git a/tests/bfs/warn_depth_prune.sh b/tests/bfs/warn_depth_prune.sh
new file mode 100644
index 0000000..0f613c8
--- /dev/null
+++ b/tests/bfs/warn_depth_prune.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs basic -warn -depth -prune 2>&1 >/dev/null)
+[ -n "$stderr" ]
diff --git a/tests/bfs/warn_exclude_path.sh b/tests/bfs/warn_exclude_path.sh
new file mode 100644
index 0000000..988544e
--- /dev/null
+++ b/tests/bfs/warn_exclude_path.sh
@@ -0,0 +1,2 @@
+stderr=$(invoke_bfs -warn -exclude basic -name '*f*' 2>&1 >/dev/null)
+[ -n "$stderr" ]
diff --git a/tests/bfs/warn_xdev_mount.out b/tests/bfs/warn_xdev_mount.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bfs/warn_xdev_mount.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bfs/warn_xdev_mount.sh b/tests/bfs/warn_xdev_mount.sh
new file mode 100644
index 0000000..5d395f6
--- /dev/null
+++ b/tests/bfs/warn_xdev_mount.sh
@@ -0,0 +1,2 @@
+# Regression test: don't crash if -mount is the last option
+bfs_diff basic -warn -xdev -mount
diff --git a/tests/bfs/xtype_depth.sh b/tests/bfs/xtype_depth.sh
new file mode 100644
index 0000000..02c8173
--- /dev/null
+++ b/tests/bfs/xtype_depth.sh
@@ -0,0 +1,2 @@
+# Make sure -xtype is considered side-effecting for facts_when_impure
+! invoke_bfs loops -xtype l -depth 100
diff --git a/tests/test_xtype_multi.out b/tests/bfs/xtype_multi.out
index 171c580..558e89c 100644
--- a/tests/test_xtype_multi.out
+++ b/tests/bfs/xtype_multi.out
@@ -1,10 +1,10 @@
links
links/deeply
-links/file
-links/hardlink
-links/skip
-links/symlink
links/deeply/nested
links/deeply/nested/dir
links/deeply/nested/file
links/deeply/nested/link
+links/file
+links/hardlink
+links/skip
+links/symlink
diff --git a/tests/bfs/xtype_multi.sh b/tests/bfs/xtype_multi.sh
new file mode 100644
index 0000000..ed20955
--- /dev/null
+++ b/tests/bfs/xtype_multi.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype f,d,c
diff --git a/tests/test_data_flow_type.out b/tests/bfs/xtype_reorder.out
index e69de29..e69de29 100644
--- a/tests/test_data_flow_type.out
+++ b/tests/bfs/xtype_reorder.out
diff --git a/tests/bfs/xtype_reorder.sh b/tests/bfs/xtype_reorder.sh
new file mode 100644
index 0000000..8d75d7d
--- /dev/null
+++ b/tests/bfs/xtype_reorder.sh
@@ -0,0 +1,4 @@
+# Make sure -xtype is not reordered in front of anything -- if -xtype runs
+# before -links 100, it will report an ELOOP error
+bfs_diff loops -links 100 -xtype l
+invoke_bfs loops -links 100 -xtype l
diff --git a/tests/bfstd.c b/tests/bfstd.c
new file mode 100644
index 0000000..07b68b0
--- /dev/null
+++ b/tests/bfstd.c
@@ -0,0 +1,93 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "bfstd.h"
+#include "diag.h"
+#include <errno.h>
+#include <langinfo.h>
+#include <stdlib.h>
+#include <string.h>
+
+/** Check the result of xdirname()/xbasename(). */
+static bool check_base_dir(const char *path, const char *dir, const char *base) {
+ bool ret = true;
+
+ char *xdir = xdirname(path);
+ bfs_verify(xdir, "xdirname(): %s", xstrerror(errno));
+ ret &= bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir);
+ free(xdir);
+
+ char *xbase = xbasename(path);
+ bfs_verify(xbase, "xbasename(): %s", xstrerror(errno));
+ ret &= bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base);
+ free(xbase);
+
+ return ret;
+}
+
+/** Check the result of wordesc(). */
+static bool check_wordesc(const char *str, const char *exp, enum wesc_flags flags) {
+ char buf[256];
+ char *end = buf + sizeof(buf);
+ char *esc = wordesc(buf, end, str, flags);
+
+ return bfs_check(esc != end)
+ && bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp);
+}
+
+bool check_bfstd(void) {
+ bool ret = true;
+
+ ret &= bfs_check(asciilen("") == 0);
+ ret &= bfs_check(asciilen("@") == 1);
+ ret &= bfs_check(asciilen("@@") == 2);
+ ret &= bfs_check(asciilen("\xFF@") == 0);
+ ret &= bfs_check(asciilen("@\xFF") == 1);
+ ret &= bfs_check(asciilen("@@@@@@@@") == 8);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@") == 16);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@@@@@@@@@") == 24);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@a\xFF@@@@@@@") == 15);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@\xFF@@@@@@@") == 16);
+ ret &= bfs_check(asciilen("@@@@@@@@@@@@@@@@a\xFF@@@@@@") == 17);
+ ret &= bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7);
+ ret &= bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8);
+ ret &= bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9);
+
+ // From man 3p basename
+ ret &= check_base_dir("usr", ".", "usr");
+ ret &= check_base_dir("usr/", ".", "usr");
+ ret &= check_base_dir("", ".", ".");
+ ret &= check_base_dir("/", "/", "/");
+ // check_base_dir("//", "/" or "//", "/" or "//");
+ ret &= check_base_dir("///", "/", "/");
+ ret &= check_base_dir("/usr/", "/", "usr");
+ ret &= check_base_dir("/usr/lib", "/usr", "lib");
+ ret &= check_base_dir("//usr//lib//", "//usr", "lib");
+ ret &= check_base_dir("/home//dwc//test", "/home//dwc", "test");
+
+ ret &= check_wordesc("", "\"\"", WESC_SHELL);
+ ret &= check_wordesc("word", "word", WESC_SHELL);
+ ret &= check_wordesc("two words", "\"two words\"", WESC_SHELL);
+ ret &= check_wordesc("word's", "\"word's\"", WESC_SHELL);
+ ret &= check_wordesc("\"word\"", "'\"word\"'", WESC_SHELL);
+ ret &= check_wordesc("\"word's\"", "'\"word'\\''s\"'", WESC_SHELL);
+ ret &= check_wordesc("\033[1mbold's\033[0m", "$'\\e[1mbold\\'s\\e[0m'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\x7F", "$'\\x7F'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("~user", "\"~user\"", WESC_SHELL);
+
+ const char *charmap = nl_langinfo(CODESET);
+ if (strcmp(charmap, "UTF-8") == 0) {
+ ret &= check_wordesc("\xF0", "$'\\xF0'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xF0\x9F", "$'\\xF0\\x9F'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xF0\x9F\x98", "$'\\xF0\\x9F\\x98'", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY);
+ ret &= check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL);
+ }
+
+ ret &= bfs_check(xstrwidth("Hello world") == 11);
+ ret &= bfs_check(xstrwidth("Hello\1world") == 10);
+
+ return ret;
+}
diff --git a/tests/bit.c b/tests/bit.c
new file mode 100644
index 0000000..674d1b2
--- /dev/null
+++ b/tests/bit.c
@@ -0,0 +1,141 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "bit.h"
+#include "diag.h"
+#include <limits.h>
+#include <stdint.h>
+#include <string.h>
+
+bfs_static_assert(UMAX_WIDTH(0x1) == 1);
+bfs_static_assert(UMAX_WIDTH(0x3) == 2);
+bfs_static_assert(UMAX_WIDTH(0x7) == 3);
+bfs_static_assert(UMAX_WIDTH(0xF) == 4);
+bfs_static_assert(UMAX_WIDTH(0xFF) == 8);
+bfs_static_assert(UMAX_WIDTH(0xFFF) == 12);
+bfs_static_assert(UMAX_WIDTH(0xFFFF) == 16);
+
+#define UWIDTH_MAX(n) (2 * ((UINTMAX_C(1) << ((n) - 1)) - 1) + 1)
+#define IWIDTH_MAX(n) UWIDTH_MAX((n) - 1)
+#define IWIDTH_MIN(n) (-(intmax_t)IWIDTH_MAX(n) - 1)
+
+bfs_static_assert(UCHAR_MAX == UWIDTH_MAX(UCHAR_WIDTH));
+bfs_static_assert(SCHAR_MIN == IWIDTH_MIN(SCHAR_WIDTH));
+bfs_static_assert(SCHAR_MAX == IWIDTH_MAX(SCHAR_WIDTH));
+
+bfs_static_assert(USHRT_MAX == UWIDTH_MAX(USHRT_WIDTH));
+bfs_static_assert(SHRT_MIN == IWIDTH_MIN(SHRT_WIDTH));
+bfs_static_assert(SHRT_MAX == IWIDTH_MAX(SHRT_WIDTH));
+
+bfs_static_assert(UINT_MAX == UWIDTH_MAX(UINT_WIDTH));
+bfs_static_assert(INT_MIN == IWIDTH_MIN(INT_WIDTH));
+bfs_static_assert(INT_MAX == IWIDTH_MAX(INT_WIDTH));
+
+bfs_static_assert(ULONG_MAX == UWIDTH_MAX(ULONG_WIDTH));
+bfs_static_assert(LONG_MIN == IWIDTH_MIN(LONG_WIDTH));
+bfs_static_assert(LONG_MAX == IWIDTH_MAX(LONG_WIDTH));
+
+bfs_static_assert(ULLONG_MAX == UWIDTH_MAX(ULLONG_WIDTH));
+bfs_static_assert(LLONG_MIN == IWIDTH_MIN(LLONG_WIDTH));
+bfs_static_assert(LLONG_MAX == IWIDTH_MAX(LLONG_WIDTH));
+
+bfs_static_assert(SIZE_MAX == UWIDTH_MAX(SIZE_WIDTH));
+bfs_static_assert(PTRDIFF_MIN == IWIDTH_MIN(PTRDIFF_WIDTH));
+bfs_static_assert(PTRDIFF_MAX == IWIDTH_MAX(PTRDIFF_WIDTH));
+
+bfs_static_assert(UINTPTR_MAX == UWIDTH_MAX(UINTPTR_WIDTH));
+bfs_static_assert(INTPTR_MIN == IWIDTH_MIN(INTPTR_WIDTH));
+bfs_static_assert(INTPTR_MAX == IWIDTH_MAX(INTPTR_WIDTH));
+
+bfs_static_assert(UINTMAX_MAX == UWIDTH_MAX(UINTMAX_WIDTH));
+bfs_static_assert(INTMAX_MIN == IWIDTH_MIN(INTMAX_WIDTH));
+bfs_static_assert(INTMAX_MAX == IWIDTH_MAX(INTMAX_WIDTH));
+
+#define check_eq(a, b) \
+ bfs_check((a) == (b), "(0x%jX) %s != %s (0x%jX)", (uintmax_t)(a), #a, #b, (uintmax_t)(b))
+
+bool check_bit(void) {
+ bool ret = true;
+
+ const char *str = "\x1\x2\x3\x4";
+ uint32_t word;
+ memcpy(&word, str, sizeof(word));
+
+#if ENDIAN_NATIVE == ENDIAN_LITTLE
+ ret &= check_eq(word, 0x04030201);
+#elif ENDIAN_NATIVE == ENDIAN_BIG
+ ret &= check_eq(word, 0x01020304);
+#else
+# warning "Skipping byte order tests on mixed/unknown-endian machine"
+#endif
+
+ ret &= check_eq(bswap((uint8_t)0x12), 0x12);
+ ret &= check_eq(bswap((uint16_t)0x1234), 0x3412);
+ ret &= check_eq(bswap((uint32_t)0x12345678), 0x78563412);
+ ret &= check_eq(bswap((uint64_t)0x1234567812345678), 0x7856341278563412);
+
+ ret &= check_eq(count_ones(0x0), 0);
+ ret &= check_eq(count_ones(0x1), 1);
+ ret &= check_eq(count_ones(0x2), 1);
+ ret &= check_eq(count_ones(0x3), 2);
+ ret &= check_eq(count_ones(0x137F), 10);
+
+ ret &= check_eq(count_zeros(0), INT_WIDTH);
+ ret &= check_eq(count_zeros(0L), LONG_WIDTH);
+ ret &= check_eq(count_zeros(0LL), LLONG_WIDTH);
+ ret &= check_eq(count_zeros((uint8_t)0), 8);
+ ret &= check_eq(count_zeros((uint16_t)0), 16);
+ ret &= check_eq(count_zeros((uint32_t)0), 32);
+ ret &= check_eq(count_zeros((uint64_t)0), 64);
+
+ ret &= check_eq(rotate_left((uint8_t)0xA1, 4), 0x1A);
+ ret &= check_eq(rotate_left((uint16_t)0x1234, 12), 0x4123);
+ ret &= check_eq(rotate_left((uint32_t)0x12345678, 20), 0x67812345);
+ ret &= check_eq(rotate_left((uint32_t)0x12345678, 0), 0x12345678);
+
+ ret &= check_eq(rotate_right((uint8_t)0xA1, 4), 0x1A);
+ ret &= check_eq(rotate_right((uint16_t)0x1234, 12), 0x2341);
+ ret &= check_eq(rotate_right((uint32_t)0x12345678, 20), 0x45678123);
+ ret &= check_eq(rotate_right((uint32_t)0x12345678, 0), 0x12345678);
+
+ for (int i = 0; i < 16; ++i) {
+ uint16_t n = (uint16_t)1 << i;
+ for (int j = i; j < 16; ++j) {
+ uint16_t m = (uint16_t)1 << j;
+ uint16_t nm = n | m;
+ ret &= check_eq(count_ones(nm), 1 + (n != m));
+ ret &= check_eq(count_zeros(nm), 15 - (n != m));
+ ret &= check_eq(leading_zeros(nm), 15 - j);
+ ret &= check_eq(trailing_zeros(nm), i);
+ ret &= check_eq(first_leading_one(nm), j + 1);
+ ret &= check_eq(first_trailing_one(nm), i + 1);
+ ret &= check_eq(bit_width(nm), j + 1);
+ ret &= check_eq(bit_floor(nm), m);
+ if (n == m) {
+ ret &= check_eq(bit_ceil(nm), m);
+ ret &= bfs_check(has_single_bit(nm));
+ } else {
+ if (j < 15) {
+ ret &= check_eq(bit_ceil(nm), (m << 1));
+ }
+ ret &= bfs_check(!has_single_bit(nm));
+ }
+ }
+ }
+
+ ret &= check_eq(leading_zeros((uint16_t)0), 16);
+ ret &= check_eq(trailing_zeros((uint16_t)0), 16);
+ ret &= check_eq(first_leading_one(0), 0);
+ ret &= check_eq(first_trailing_one(0), 0);
+ ret &= check_eq(bit_width(0), 0);
+ ret &= check_eq(bit_floor(0), 0);
+ ret &= check_eq(bit_ceil(0), 1);
+
+ ret &= bfs_check(!has_single_bit(0));
+ ret &= bfs_check(!has_single_bit(UINT32_MAX));
+ ret &= bfs_check(has_single_bit((uint32_t)1 << (UINT_WIDTH - 1)));
+
+ return ret;
+}
diff --git a/tests/test_E.out b/tests/bsd/E.out
index 0f0971e..0f0971e 100644
--- a/tests/test_E.out
+++ b/tests/bsd/E.out
diff --git a/tests/bsd/E.sh b/tests/bsd/E.sh
new file mode 100644
index 0000000..5d97178
--- /dev/null
+++ b/tests/bsd/E.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -E . -regex '\./(\()'
diff --git a/tests/test_H_mnewer.out b/tests/bsd/H_mnewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_H_mnewer.out
+++ b/tests/bsd/H_mnewer.out
diff --git a/tests/bsd/H_mnewer.sh b/tests/bsd/H_mnewer.sh
new file mode 100644
index 0000000..94fe08b
--- /dev/null
+++ b/tests/bsd/H_mnewer.sh
@@ -0,0 +1 @@
+bfs_diff -H times -mnewer times/l
diff --git a/tests/bsd/L_acl.out b/tests/bsd/L_acl.out
new file mode 100644
index 0000000..dd89800
--- /dev/null
+++ b/tests/bsd/L_acl.out
@@ -0,0 +1,2 @@
+./acl
+./link
diff --git a/tests/bsd/L_acl.sh b/tests/bsd/L_acl.sh
new file mode 100644
index 0000000..a3fcbc8
--- /dev/null
+++ b/tests/bsd/L_acl.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+invoke_bfs . -quit -acl || skip
+
+"$XTOUCH" normal acl
+set_acl acl || skip
+ln -s acl link
+
+bfs_diff -L . -acl
diff --git a/tests/bsd/L_xattr.out b/tests/bsd/L_xattr.out
new file mode 100644
index 0000000..21eb50f
--- /dev/null
+++ b/tests/bsd/L_xattr.out
@@ -0,0 +1,3 @@
+./link
+./xattr
+./xattr_2
diff --git a/tests/bsd/L_xattr.sh b/tests/bsd/L_xattr.sh
new file mode 100644
index 0000000..f8b56d8
--- /dev/null
+++ b/tests/bsd/L_xattr.sh
@@ -0,0 +1,3 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+bfs_diff -L . -xattr
diff --git a/tests/bsd/L_xattrname.out b/tests/bsd/L_xattrname.out
new file mode 100644
index 0000000..9e4c172
--- /dev/null
+++ b/tests/bsd/L_xattrname.out
@@ -0,0 +1,2 @@
+./link
+./xattr
diff --git a/tests/bsd/L_xattrname.sh b/tests/bsd/L_xattrname.sh
new file mode 100644
index 0000000..8108d57
--- /dev/null
+++ b/tests/bsd/L_xattrname.sh
@@ -0,0 +1,11 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+
+case "$UNAME" in
+ Darwin|FreeBSD)
+ bfs_diff -L . -xattrname bfs_test
+ ;;
+ *)
+ bfs_diff -L . -xattrname security.bfs_test
+ ;;
+esac
diff --git a/tests/test_X.out b/tests/bsd/X.out
index 008297b..afa84f7 100644
--- a/tests/test_X.out
+++ b/tests/bsd/X.out
@@ -1,17 +1,19 @@
weirdnames
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/...
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/...
weirdnames/.../h
+weirdnames/[
+weirdnames/[/k
diff --git a/tests/bsd/X.sh b/tests/bsd/X.sh
new file mode 100644
index 0000000..54000cf
--- /dev/null
+++ b/tests/bsd/X.sh
@@ -0,0 +1 @@
+! bfs_diff -X weirdnames
diff --git a/tests/bsd/acl.out b/tests/bsd/acl.out
new file mode 100644
index 0000000..92e2f67
--- /dev/null
+++ b/tests/bsd/acl.out
@@ -0,0 +1 @@
+./acl
diff --git a/tests/bsd/acl.sh b/tests/bsd/acl.sh
new file mode 100644
index 0000000..a13c75f
--- /dev/null
+++ b/tests/bsd/acl.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+invoke_bfs . -quit -acl || skip
+
+"$XTOUCH" normal acl
+set_acl acl || skip
+ln -s acl link
+
+bfs_diff . -acl
diff --git a/tests/test_asince.out b/tests/bsd/asince.out
index 650e550..650e550 100644
--- a/tests/test_asince.out
+++ b/tests/bsd/asince.out
diff --git a/tests/bsd/asince.sh b/tests/bsd/asince.sh
new file mode 100644
index 0000000..32d5228
--- /dev/null
+++ b/tests/bsd/asince.sh
@@ -0,0 +1 @@
+bfs_diff times -asince 1991-12-14T00:01
diff --git a/tests/bsd/d_path.out b/tests/bsd/d_path.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/d_path.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bsd/d_path.sh b/tests/bsd/d_path.sh
new file mode 100644
index 0000000..010f76f
--- /dev/null
+++ b/tests/bsd/d_path.sh
@@ -0,0 +1 @@
+bfs_diff -d basic
diff --git a/tests/test_data_flow_depth.out b/tests/bsd/data_flow_depth.out
index 350b489..ab127ec 100644
--- a/tests/test_data_flow_depth.out
+++ b/tests/bsd/data_flow_depth.out
@@ -3,6 +3,6 @@ basic/e/f
basic/g/h
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
diff --git a/tests/bsd/data_flow_depth.sh b/tests/bsd/data_flow_depth.sh
new file mode 100644
index 0000000..cd5d6b2
--- /dev/null
+++ b/tests/bsd/data_flow_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth +1 -depth -4
diff --git a/tests/bsd/data_flow_sparse.out b/tests/bsd/data_flow_sparse.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/data_flow_sparse.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bsd/data_flow_sparse.sh b/tests/bsd/data_flow_sparse.sh
new file mode 100644
index 0000000..bd7e1f7
--- /dev/null
+++ b/tests/bsd/data_flow_sparse.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -sparse -not -sparse \) -o \( -sparse -o -not -sparse \)
diff --git a/tests/test_depth_depth_n.out b/tests/bsd/depth_depth_n.out
index 3bfc1d3..3bfc1d3 100644
--- a/tests/test_depth_depth_n.out
+++ b/tests/bsd/depth_depth_n.out
diff --git a/tests/bsd/depth_depth_n.sh b/tests/bsd/depth_depth_n.sh
new file mode 100644
index 0000000..5989b3c
--- /dev/null
+++ b/tests/bsd/depth_depth_n.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth 2
diff --git a/tests/test_depth_depth_n_minus.out b/tests/bsd/depth_depth_n_minus.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_depth_n_minus.out
+++ b/tests/bsd/depth_depth_n_minus.out
diff --git a/tests/bsd/depth_depth_n_minus.sh b/tests/bsd/depth_depth_n_minus.sh
new file mode 100644
index 0000000..1d8ac79
--- /dev/null
+++ b/tests/bsd/depth_depth_n_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth -2
diff --git a/tests/test_depth_depth_n_plus.out b/tests/bsd/depth_depth_n_plus.out
index 847995d..847995d 100644
--- a/tests/test_depth_depth_n_plus.out
+++ b/tests/bsd/depth_depth_n_plus.out
diff --git a/tests/bsd/depth_depth_n_plus.sh b/tests/bsd/depth_depth_n_plus.sh
new file mode 100644
index 0000000..64e392b
--- /dev/null
+++ b/tests/bsd/depth_depth_n_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -depth +2
diff --git a/tests/test_depth_n.out b/tests/bsd/depth_n.out
index 3bfc1d3..3bfc1d3 100644
--- a/tests/test_depth_n.out
+++ b/tests/bsd/depth_n.out
diff --git a/tests/bsd/depth_n.sh b/tests/bsd/depth_n.sh
new file mode 100644
index 0000000..4852952
--- /dev/null
+++ b/tests/bsd/depth_n.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth 2
diff --git a/tests/test_depth_maxdepth_1.out b/tests/bsd/depth_n_minus.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_maxdepth_1.out
+++ b/tests/bsd/depth_n_minus.out
diff --git a/tests/bsd/depth_n_minus.sh b/tests/bsd/depth_n_minus.sh
new file mode 100644
index 0000000..192bf8a
--- /dev/null
+++ b/tests/bsd/depth_n_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -2
diff --git a/tests/test_depth_n_plus.out b/tests/bsd/depth_n_plus.out
index 847995d..847995d 100644
--- a/tests/test_depth_n_plus.out
+++ b/tests/bsd/depth_n_plus.out
diff --git a/tests/bsd/depth_n_plus.sh b/tests/bsd/depth_n_plus.sh
new file mode 100644
index 0000000..858e1c4
--- /dev/null
+++ b/tests/bsd/depth_n_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth +2
diff --git a/tests/bsd/depth_overflow.out b/tests/bsd/depth_overflow.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/depth_overflow.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bsd/depth_overflow.sh b/tests/bsd/depth_overflow.sh
new file mode 100644
index 0000000..4685d03
--- /dev/null
+++ b/tests/bsd/depth_overflow.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth -4294967296
diff --git a/tests/test_exit.out b/tests/bsd/exit.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_exit.out
+++ b/tests/bsd/exit.out
diff --git a/tests/bsd/exit.sh b/tests/bsd/exit.sh
new file mode 100644
index 0000000..248349c
--- /dev/null
+++ b/tests/bsd/exit.sh
@@ -0,0 +1,5 @@
+check_exit 42 invoke_bfs basic -name foo -exit 42
+
+check_exit 0 invoke_bfs basic -name qux -exit 42
+
+bfs_diff basic/g -print -name g -exit
diff --git a/tests/test_exclude_mindepth.out b/tests/bsd/exit_no_implicit_print.out
index e69de29..e69de29 100644
--- a/tests/test_exclude_mindepth.out
+++ b/tests/bsd/exit_no_implicit_print.out
diff --git a/tests/bsd/exit_no_implicit_print.sh b/tests/bsd/exit_no_implicit_print.sh
new file mode 100644
index 0000000..c48b43c
--- /dev/null
+++ b/tests/bsd/exit_no_implicit_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -not -name foo -o -exit
diff --git a/tests/test_f.out b/tests/bsd/f.out
index 2998538..77eac77 100644
--- a/tests/test_f.out
+++ b/tests/bsd/f.out
@@ -1,4 +1,4 @@
(
--
(/b
+-
-/a
diff --git a/tests/bsd/f.sh b/tests/bsd/f.sh
new file mode 100644
index 0000000..42d2dfd
--- /dev/null
+++ b/tests/bsd/f.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -f '-' -f '('
diff --git a/tests/bsd/f_incomplete.sh b/tests/bsd/f_incomplete.sh
new file mode 100644
index 0000000..0dfb19f
--- /dev/null
+++ b/tests/bsd/f_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs -f
diff --git a/tests/bsd/flags.out b/tests/bsd/flags.out
new file mode 100644
index 0000000..3216ff5
--- /dev/null
+++ b/tests/bsd/flags.out
@@ -0,0 +1 @@
+./bar
diff --git a/tests/bsd/flags.sh b/tests/bsd/flags.sh
new file mode 100644
index 0000000..eb9bc22
--- /dev/null
+++ b/tests/bsd/flags.sh
@@ -0,0 +1,8 @@
+invoke_bfs . -quit -flags offline || skip
+
+cd "$TEST"
+
+"$XTOUCH" foo bar
+chflags offline bar || skip
+
+bfs_diff . -flags -offline,nohidden
diff --git a/tests/bsd/gid_name.out b/tests/bsd/gid_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/gid_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bsd/gid_name.sh b/tests/bsd/gid_name.sh
new file mode 100644
index 0000000..c7e0e71
--- /dev/null
+++ b/tests/bsd/gid_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "$(id -gn)"
diff --git a/tests/test_H_newer.out b/tests/bsd/mnewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_H_newer.out
+++ b/tests/bsd/mnewer.out
diff --git a/tests/bsd/mnewer.sh b/tests/bsd/mnewer.sh
new file mode 100644
index 0000000..5d9f1a7
--- /dev/null
+++ b/tests/bsd/mnewer.sh
@@ -0,0 +1 @@
+bfs_diff times -mnewer times/a
diff --git a/tests/test_msince.out b/tests/bsd/msince.out
index 650e550..650e550 100644
--- a/tests/test_msince.out
+++ b/tests/bsd/msince.out
diff --git a/tests/bsd/msince.sh b/tests/bsd/msince.sh
new file mode 100644
index 0000000..ec22f02
--- /dev/null
+++ b/tests/bsd/msince.sh
@@ -0,0 +1 @@
+bfs_diff times -msince 1991-12-14T00:01
diff --git a/tests/bsd/mtime_bad_unit.sh b/tests/bsd/mtime_bad_unit.sh
new file mode 100644
index 0000000..6e2caf1
--- /dev/null
+++ b/tests/bsd/mtime_bad_unit.sh
@@ -0,0 +1 @@
+! invoke_bfs times -mtime +1q
diff --git a/tests/bsd/mtime_missing_unit.sh b/tests/bsd/mtime_missing_unit.sh
new file mode 100644
index 0000000..f6b1f93
--- /dev/null
+++ b/tests/bsd/mtime_missing_unit.sh
@@ -0,0 +1 @@
+! invoke_bfs times -mtime +1w2
diff --git a/tests/test_mtime_units.out b/tests/bsd/mtime_units.out
index f7f63b0..f7f63b0 100644
--- a/tests/test_mtime_units.out
+++ b/tests/bsd/mtime_units.out
diff --git a/tests/bsd/mtime_units.sh b/tests/bsd/mtime_units.sh
new file mode 100644
index 0000000..a1e587e
--- /dev/null
+++ b/tests/bsd/mtime_units.sh
@@ -0,0 +1 @@
+bfs_diff times -mtime +500w400d300h200m100s
diff --git a/tests/test_okdir_stdin.out b/tests/bsd/okdir_stdin.out
index ef2a68b..ef2a68b 100644
--- a/tests/test_okdir_stdin.out
+++ b/tests/bsd/okdir_stdin.out
diff --git a/tests/bsd/okdir_stdin.sh b/tests/bsd/okdir_stdin.sh
new file mode 100644
index 0000000..7908ac0
--- /dev/null
+++ b/tests/bsd/okdir_stdin.sh
@@ -0,0 +1,2 @@
+# -okdir should *not* close stdin
+yes | bfs_diff basic -okdir bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
diff --git a/tests/test_perm_000_minus.out b/tests/bsd/perm_000_plus.out
index d7494b8..d7494b8 100644
--- a/tests/test_perm_000_minus.out
+++ b/tests/bsd/perm_000_plus.out
diff --git a/tests/bsd/perm_000_plus.sh b/tests/bsd/perm_000_plus.sh
new file mode 100644
index 0000000..9ab3146
--- /dev/null
+++ b/tests/bsd/perm_000_plus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +000
diff --git a/tests/test_perm_222_plus.out b/tests/bsd/perm_222_plus.out
index 9a5b95a..9a5b95a 100644
--- a/tests/test_perm_222_plus.out
+++ b/tests/bsd/perm_222_plus.out
diff --git a/tests/bsd/perm_222_plus.sh b/tests/bsd/perm_222_plus.sh
new file mode 100644
index 0000000..ac3c4eb
--- /dev/null
+++ b/tests/bsd/perm_222_plus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +222
diff --git a/tests/test_perm_644_plus.out b/tests/bsd/perm_644_plus.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_644_plus.out
+++ b/tests/bsd/perm_644_plus.out
diff --git a/tests/bsd/perm_644_plus.sh b/tests/bsd/perm_644_plus.sh
new file mode 100644
index 0000000..b3f5bc6
--- /dev/null
+++ b/tests/bsd/perm_644_plus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm +644
diff --git a/tests/test_printx.out b/tests/bsd/printx.out
index 53c7547..04bf9a9 100644
--- a/tests/test_printx.out
+++ b/tests/bsd/printx.out
@@ -1,21 +1,23 @@
weirdnames
weirdnames/!
weirdnames/!-
-weirdnames/(
-weirdnames/(-
-weirdnames/)
-weirdnames/,
-weirdnames/-
-weirdnames/...
-weirdnames/\
-weirdnames/\\
weirdnames/!-/e
weirdnames/!/d
+weirdnames/(
+weirdnames/(-
weirdnames/(-/c
weirdnames/(/b
+weirdnames/)
weirdnames/)/g
+weirdnames/,
weirdnames/,/f
+weirdnames/-
weirdnames/-/a
+weirdnames/...
weirdnames/.../h
+weirdnames/[
+weirdnames/[/k
+weirdnames/\
weirdnames/\ /j
+weirdnames/\\
weirdnames/\\/i
diff --git a/tests/bsd/printx.sh b/tests/bsd/printx.sh
new file mode 100644
index 0000000..cb24aab
--- /dev/null
+++ b/tests/bsd/printx.sh
@@ -0,0 +1 @@
+bfs_diff weirdnames -printx
diff --git a/tests/test_and_false_or_true.out b/tests/bsd/quit_implicit_print.out
index 15a13db..15a13db 100644
--- a/tests/test_and_false_or_true.out
+++ b/tests/bsd/quit_implicit_print.out
diff --git a/tests/bsd/quit_implicit_print.sh b/tests/bsd/quit_implicit_print.sh
new file mode 100644
index 0000000..ea8fd5d
--- /dev/null
+++ b/tests/bsd/quit_implicit_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -name basic -o -quit
diff --git a/tests/bsd/rm.out b/tests/bsd/rm.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/bsd/rm.out
@@ -0,0 +1 @@
+.
diff --git a/tests/bsd/rm.sh b/tests/bsd/rm.sh
new file mode 100644
index 0000000..595d514
--- /dev/null
+++ b/tests/bsd/rm.sh
@@ -0,0 +1,4 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar/baz
+invoke_bfs . -rm
+bfs_diff .
diff --git a/tests/test_s.out b/tests/bsd/s.out
index e736cb5..6b790c2 100644
--- a/tests/test_s.out
+++ b/tests/bsd/s.out
@@ -8,4 +8,5 @@ weirdnames/)
weirdnames/,
weirdnames/-
weirdnames/...
+weirdnames/[
weirdnames/\
diff --git a/tests/bsd/s.sh b/tests/bsd/s.sh
new file mode 100644
index 0000000..52f8eb3
--- /dev/null
+++ b/tests/bsd/s.sh
@@ -0,0 +1,2 @@
+invoke_bfs -s weirdnames -maxdepth 1 >"$OUT"
+diff_output
diff --git a/tests/bsd/s_quit.out b/tests/bsd/s_quit.out
new file mode 100644
index 0000000..5ea492b
--- /dev/null
+++ b/tests/bsd/s_quit.out
@@ -0,0 +1 @@
+basic/j/foo
diff --git a/tests/bsd/s_quit.sh b/tests/bsd/s_quit.sh
new file mode 100644
index 0000000..6bd55ab
--- /dev/null
+++ b/tests/bsd/s_quit.sh
@@ -0,0 +1,4 @@
+# Regression test: bfs -S ids -s -name foo -quit would not actually quit,
+# ending up in a confused state and erroring/crashing
+
+bfs_diff -s basic -name foo -print -quit
diff --git a/tests/test_size_T.out b/tests/bsd/size_T.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_T.out
+++ b/tests/bsd/size_T.out
diff --git a/tests/bsd/size_T.sh b/tests/bsd/size_T.sh
new file mode 100644
index 0000000..1023a10
--- /dev/null
+++ b/tests/bsd/size_T.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size 1T
diff --git a/tests/bsd/sparse.out b/tests/bsd/sparse.out
new file mode 100644
index 0000000..52dcf49
--- /dev/null
+++ b/tests/bsd/sparse.out
@@ -0,0 +1 @@
+mnt/sparse
diff --git a/tests/bsd/sparse.sh b/tests/bsd/sparse.sh
new file mode 100644
index 0000000..7fcdeed
--- /dev/null
+++ b/tests/bsd/sparse.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkdir mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+truncate -s 1M mnt/sparse
+dd if=/dev/zero of=mnt/dense bs=1M count=1
+
+bfs_diff mnt -type f -sparse
diff --git a/tests/bsd/type_w.out b/tests/bsd/type_w.out
new file mode 100644
index 0000000..a20a4f3
--- /dev/null
+++ b/tests/bsd/type_w.out
@@ -0,0 +1,34 @@
+1: -rw-r--r-- mnt/lower/bar
+1: -rw-r--r-- mnt/lower/baz
+1: -rw-r--r-- mnt/lower/foo
+1: -rw-r--r-- mnt/upper/baz/qux
+1: -rw-r--r-- mnt/upper/foo
+1: drwxr-xr-x mnt/lower
+1: drwxr-xr-x mnt/upper
+1: drwxr-xr-x mnt/upper/baz
+2: w--------- mnt/upper/bar
+3: -rw-r--r-- mnt/lower/bar
+3: -rw-r--r-- mnt/lower/baz
+3: -rw-r--r-- mnt/lower/foo
+3: -rw-r--r-- mnt/upper/baz/qux
+3: -rw-r--r-- mnt/upper/foo
+3: drwxr-xr-x mnt/lower
+3: drwxr-xr-x mnt/upper
+3: drwxr-xr-x mnt/upper/baz
+3: w--------- mnt/upper/bar
+4: -rw-r--r-- mnt/lower/bar
+4: -rw-r--r-- mnt/lower/baz
+4: -rw-r--r-- mnt/lower/foo
+4: -rw-r--r-- mnt/upper/baz/qux
+4: drwxr-xr-x mnt/lower
+4: drwxr-xr-x mnt/upper
+4: drwxr-xr-x mnt/upper/baz
+5: w--------- mnt/upper/bar
+6: -rw-r--r-- mnt/lower/bar
+6: -rw-r--r-- mnt/lower/baz
+6: -rw-r--r-- mnt/lower/foo
+6: -rw-r--r-- mnt/upper/baz/qux
+6: drwxr-xr-x mnt/lower
+6: drwxr-xr-x mnt/upper
+6: drwxr-xr-x mnt/upper/baz
+6: w--------- mnt/upper/bar
diff --git a/tests/bsd/type_w.sh b/tests/bsd/type_w.sh
new file mode 100644
index 0000000..3aa50d5
--- /dev/null
+++ b/tests/bsd/type_w.sh
@@ -0,0 +1,56 @@
+# Only ffs supports whiteouts on FreeBSD
+command -v mdconfig &>/dev/null || skip
+command -v newfs &>/dev/null || skip
+
+cd "$TEST"
+
+# Create a ramdisk
+if command -v truncate &>/dev/null; then
+ truncate -s1M img
+else
+ dd if=/dev/zero of=img bs=1k count=1k
+fi
+md=$(bfs_sudo mdconfig img) || skip
+defer bfs_sudo mdconfig -du "$md"
+
+# Make an ffs filesystem
+bfs_sudo newfs -n "/dev/$md" >&2 || skip
+mkdir mnt
+
+# Mount it
+bfs_sudo mount "/dev/$md" mnt || skip
+defer bfs_sudo umount mnt
+
+# Make it owned by us
+bfs_sudo chown "$(id -u):$(id -g)" mnt
+"$XTOUCH" -p mnt/{lower/{foo,bar,baz},upper/{bar,baz/qux}}
+
+# Mount a union filesystem within it
+bfs_sudo mount -t unionfs -o below mnt/{lower,upper}
+defer bfs_sudo umount mnt/upper
+
+# Create a whiteout
+rm mnt/upper/bar
+
+# FreeBSD find doesn't have -printf, so munge -ls output
+munge_ls() {
+ sed -En 's|.*([-drwx]{10}).*(mnt/.*)|'"$1"': \1 \2|p'
+}
+
+# Do a few tests in one
+{
+ # Normally, we shouldn't see the whiteouts
+ invoke_bfs mnt -ls | munge_ls 1
+ # -type w adds whiteouts to the output
+ invoke_bfs mnt -type w -ls | munge_ls 2
+ # So this is not the same as test 1
+ invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 3
+ # Unmount the unionfs
+ pop_defer
+ # Now repeat the same tests
+ invoke_bfs mnt -ls | munge_ls 4
+ invoke_bfs mnt -type w -ls | munge_ls 5
+ invoke_bfs mnt \( -type w -or -not -type w \) -ls | munge_ls 6
+} >"$OUT"
+sort_output
+diff_output
diff --git a/tests/bsd/uid_name.out b/tests/bsd/uid_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/bsd/uid_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/bsd/uid_name.sh b/tests/bsd/uid_name.sh
new file mode 100644
index 0000000..7d3ba82
--- /dev/null
+++ b/tests/bsd/uid_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "$(id -un)"
diff --git a/tests/bsd/xattr.out b/tests/bsd/xattr.out
new file mode 100644
index 0000000..0afed35
--- /dev/null
+++ b/tests/bsd/xattr.out
@@ -0,0 +1,3 @@
+./xattr
+./xattr_2
+./xattr_link
diff --git a/tests/bsd/xattr.sh b/tests/bsd/xattr.sh
new file mode 100644
index 0000000..68f729a
--- /dev/null
+++ b/tests/bsd/xattr.sh
@@ -0,0 +1,3 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+bfs_diff . -xattr
diff --git a/tests/bsd/xattrname.out b/tests/bsd/xattrname.out
new file mode 100644
index 0000000..ef732bd
--- /dev/null
+++ b/tests/bsd/xattrname.out
@@ -0,0 +1,2 @@
+./xattr
+./xattr_link
diff --git a/tests/bsd/xattrname.sh b/tests/bsd/xattrname.sh
new file mode 100644
index 0000000..38b111a
--- /dev/null
+++ b/tests/bsd/xattrname.sh
@@ -0,0 +1,11 @@
+invoke_bfs . -quit -xattr || skip
+make_xattrs || skip
+
+case "$UNAME" in
+ Darwin|FreeBSD)
+ bfs_diff . -xattrname bfs_test
+ ;;
+ *)
+ bfs_diff . -xattrname security.bfs_test
+ ;;
+esac
diff --git a/tests/color.sh b/tests/color.sh
new file mode 100644
index 0000000..805d2b8
--- /dev/null
+++ b/tests/color.sh
@@ -0,0 +1,37 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Colored output
+
+# Common escape sequences
+BLD=$'\e[01m'
+RED=$'\e[01;31m'
+GRN=$'\e[01;32m'
+YLW=$'\e[01;33m'
+BLU=$'\e[01;34m'
+MAG=$'\e[01;35m'
+CYN=$'\e[01;36m'
+RST=$'\e[0m'
+
+# Check if we should color output to the given fd
+color_fd() {
+ [ -z "${NO_COLOR:-}" ] && [ -t "$1" ]
+}
+
+# Cache the color status for std{out,err}
+color_fd 1 && COLOR_STDOUT=1 || COLOR_STDOUT=0
+color_fd 2 && COLOR_STDERR=1 || COLOR_STDERR=0
+
+# Save this in case the tests unset PATH
+SED=$(command -v sed)
+
+# Filter out escape sequences if necessary
+color() {
+ if color_fd 1; then
+ "$@"
+ else
+ "$@" | "$SED" $'s/\e\\[[^m]*m//g'
+ fi
+}
diff --git a/tests/test_anewer.out b/tests/common/H_newer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_anewer.out
+++ b/tests/common/H_newer.out
diff --git a/tests/common/H_newer.sh b/tests/common/H_newer.sh
new file mode 100644
index 0000000..c72bff7
--- /dev/null
+++ b/tests/common/H_newer.sh
@@ -0,0 +1 @@
+bfs_diff -H times -newer times/l
diff --git a/tests/test_H_broken.out b/tests/common/H_samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_H_broken.out
+++ b/tests/common/H_samefile_broken.out
diff --git a/tests/common/H_samefile_broken.sh b/tests/common/H_samefile_broken.sh
new file mode 100644
index 0000000..7a3366d
--- /dev/null
+++ b/tests/common/H_samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/broken
diff --git a/tests/test_H_notdir.out b/tests/common/H_samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_H_notdir.out
+++ b/tests/common/H_samefile_notdir.out
diff --git a/tests/common/H_samefile_notdir.sh b/tests/common/H_samefile_notdir.sh
new file mode 100644
index 0000000..25ad88d
--- /dev/null
+++ b/tests/common/H_samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/notdir
diff --git a/tests/test_H_samefile_symlink.out b/tests/common/H_samefile_symlink.out
index 996ffc8..996ffc8 100644
--- a/tests/test_H_samefile_symlink.out
+++ b/tests/common/H_samefile_symlink.out
diff --git a/tests/common/H_samefile_symlink.sh b/tests/common/H_samefile_symlink.sh
new file mode 100644
index 0000000..c73ddb2
--- /dev/null
+++ b/tests/common/H_samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff -H links -samefile links/symlink
diff --git a/tests/test_false.out b/tests/common/L_ilname.out
index e69de29..e69de29 100644
--- a/tests/test_false.out
+++ b/tests/common/L_ilname.out
diff --git a/tests/common/L_ilname.sh b/tests/common/L_ilname.sh
new file mode 100644
index 0000000..e0495ed
--- /dev/null
+++ b/tests/common/L_ilname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ilname PATTERN || skip
+bfs_diff -L links -ilname '[AQ]'
diff --git a/tests/test_ilname.out b/tests/common/L_lname.out
index e69de29..e69de29 100644
--- a/tests/test_ilname.out
+++ b/tests/common/L_lname.out
diff --git a/tests/common/L_lname.sh b/tests/common/L_lname.sh
new file mode 100644
index 0000000..65b9da5
--- /dev/null
+++ b/tests/common/L_lname.sh
@@ -0,0 +1 @@
+bfs_diff -L links -lname '[aq]'
diff --git a/tests/common/L_ls.sh b/tests/common/L_ls.sh
new file mode 100644
index 0000000..7ee2b44
--- /dev/null
+++ b/tests/common/L_ls.sh
@@ -0,0 +1 @@
+invoke_bfs -L rainbow -ls >"$OUT"
diff --git a/tests/common/L_mount.out b/tests/common/L_mount.out
new file mode 100644
index 0000000..788579d
--- /dev/null
+++ b/tests/common/L_mount.out
@@ -0,0 +1,5 @@
+.
+./foo
+./foo/bar
+./foo/qux
+./mnt
diff --git a/tests/common/L_mount.sh b/tests/common/L_mount.sh
new file mode 100644
index 0000000..fd8042a
--- /dev/null
+++ b/tests/common/L_mount.sh
@@ -0,0 +1,13 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+ln -s ../mnt foo/bar
+"$XTOUCH" mnt/baz
+ln -s ../mnt/baz foo/qux
+
+bfs_diff -L . -mount
diff --git a/tests/test_H_samefile_broken.out b/tests/common/L_samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_H_samefile_broken.out
+++ b/tests/common/L_samefile_broken.out
diff --git a/tests/common/L_samefile_broken.sh b/tests/common/L_samefile_broken.sh
new file mode 100644
index 0000000..5f860cc
--- /dev/null
+++ b/tests/common/L_samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff -L links -samefile links/broken
diff --git a/tests/test_H_samefile_notdir.out b/tests/common/L_samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_H_samefile_notdir.out
+++ b/tests/common/L_samefile_notdir.out
diff --git a/tests/common/L_samefile_notdir.sh b/tests/common/L_samefile_notdir.sh
new file mode 100644
index 0000000..9b63429
--- /dev/null
+++ b/tests/common/L_samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -L links -samefile links/notdir
diff --git a/tests/test_L_samefile_symlink.out b/tests/common/L_samefile_symlink.out
index 222ac78..222ac78 100644
--- a/tests/test_L_samefile_symlink.out
+++ b/tests/common/L_samefile_symlink.out
diff --git a/tests/common/L_samefile_symlink.sh b/tests/common/L_samefile_symlink.sh
new file mode 100644
index 0000000..4a7a8dd
--- /dev/null
+++ b/tests/common/L_samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff -L links -samefile links/symlink
diff --git a/tests/test_H.out b/tests/common/P.out
index ff635ff..ff635ff 100644
--- a/tests/test_H.out
+++ b/tests/common/P.out
diff --git a/tests/common/P.sh b/tests/common/P.sh
new file mode 100644
index 0000000..a7a09d1
--- /dev/null
+++ b/tests/common/P.sh
@@ -0,0 +1 @@
+bfs_diff -P links/deeply/nested/dir
diff --git a/tests/test_H_slash.out b/tests/common/P_slash.out
index df7701b..df7701b 100644
--- a/tests/test_H_slash.out
+++ b/tests/common/P_slash.out
diff --git a/tests/common/P_slash.sh b/tests/common/P_slash.sh
new file mode 100644
index 0000000..9b9ffa0
--- /dev/null
+++ b/tests/common/P_slash.sh
@@ -0,0 +1 @@
+bfs_diff -P links/deeply/nested/dir/
diff --git a/tests/test_mnewer.out b/tests/common/anewer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_mnewer.out
+++ b/tests/common/anewer.out
diff --git a/tests/common/anewer.sh b/tests/common/anewer.sh
new file mode 100644
index 0000000..0cdd759
--- /dev/null
+++ b/tests/common/anewer.sh
@@ -0,0 +1 @@
+bfs_diff times -anewer times/a
diff --git a/tests/common/delete.out b/tests/common/delete.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/common/delete.out
@@ -0,0 +1 @@
+.
diff --git a/tests/common/delete.sh b/tests/common/delete.sh
new file mode 100644
index 0000000..638f307
--- /dev/null
+++ b/tests/common/delete.sh
@@ -0,0 +1,4 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar/baz
+invoke_bfs . -delete
+bfs_diff .
diff --git a/tests/common/delete_error.out b/tests/common/delete_error.out
new file mode 100644
index 0000000..b6b6505
--- /dev/null
+++ b/tests/common/delete_error.out
@@ -0,0 +1,8 @@
+.
+.
+./baz
+./baz
+./baz/qux
+./baz/qux
+./foo
+./foo/bar
diff --git a/tests/common/delete_error.sh b/tests/common/delete_error.sh
new file mode 100644
index 0000000..e6327f3
--- /dev/null
+++ b/tests/common/delete_error.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/bar baz/qux
+chmod -w foo
+defer chmod +w foo
+
+! invoke_bfs . -print -delete -print >"$OUT" || fail
+sort_output
+diff_output
diff --git a/tests/common/delete_many.out b/tests/common/delete_many.out
new file mode 100644
index 0000000..9c558e3
--- /dev/null
+++ b/tests/common/delete_many.out
@@ -0,0 +1 @@
+.
diff --git a/tests/common/delete_many.sh b/tests/common/delete_many.sh
new file mode 100644
index 0000000..48fe4c2
--- /dev/null
+++ b/tests/common/delete_many.sh
@@ -0,0 +1,8 @@
+# Test for https://github.com/tavianator/bfs/issues/67
+
+cd "$TEST"
+mkdir foo
+"$XTOUCH" foo/{1..256}
+
+invoke_bfs foo -delete
+bfs_diff .
diff --git a/tests/test_depth_n_minus.out b/tests/common/depth_maxdepth_1.out
index 7575ae4..7575ae4 100644
--- a/tests/test_depth_n_minus.out
+++ b/tests/common/depth_maxdepth_1.out
diff --git a/tests/common/depth_maxdepth_1.sh b/tests/common/depth_maxdepth_1.sh
new file mode 100644
index 0000000..4b7e538
--- /dev/null
+++ b/tests/common/depth_maxdepth_1.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 1 -depth
diff --git a/tests/test_depth_maxdepth_2.out b/tests/common/depth_maxdepth_2.out
index b79fef1..c53041e 100644
--- a/tests/test_depth_maxdepth_2.out
+++ b/tests/common/depth_maxdepth_2.out
@@ -2,15 +2,15 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
+basic/l
basic/l/foo
diff --git a/tests/common/depth_maxdepth_2.sh b/tests/common/depth_maxdepth_2.sh
new file mode 100644
index 0000000..2c49a65
--- /dev/null
+++ b/tests/common/depth_maxdepth_2.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 2 -depth
diff --git a/tests/test_depth_mindepth_1.out b/tests/common/depth_mindepth_1.out
index 1f00c58..3b461cf 100644
--- a/tests/test_depth_mindepth_1.out
+++ b/tests/common/depth_mindepth_1.out
@@ -1,18 +1,18 @@
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/depth_mindepth_1.sh b/tests/common/depth_mindepth_1.sh
new file mode 100644
index 0000000..868d9e1
--- /dev/null
+++ b/tests/common/depth_mindepth_1.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 1 -depth
diff --git a/tests/test_depth_mindepth_2.out b/tests/common/depth_mindepth_2.out
index 4198bf9..6ccd80a 100644
--- a/tests/test_depth_mindepth_2.out
+++ b/tests/common/depth_mindepth_2.out
@@ -3,7 +3,7 @@ basic/e/f
basic/g/h
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/depth_mindepth_2.sh b/tests/common/depth_mindepth_2.sh
new file mode 100644
index 0000000..2031b2c
--- /dev/null
+++ b/tests/common/depth_mindepth_2.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 2 -depth
diff --git a/tests/test_double_dash.out b/tests/common/double_dash.out
index 774cc7c..774cc7c 100644
--- a/tests/test_double_dash.out
+++ b/tests/common/double_dash.out
diff --git a/tests/common/double_dash.sh b/tests/common/double_dash.sh
new file mode 100644
index 0000000..1a7a118
--- /dev/null
+++ b/tests/common/double_dash.sh
@@ -0,0 +1,2 @@
+cd basic
+bfs_diff -- . -type f
diff --git a/tests/test_empty.out b/tests/common/empty.out
index 81bdb0a..a0f4b76 100644
--- a/tests/test_empty.out
+++ b/tests/common/empty.out
@@ -1,8 +1,8 @@
basic/a
basic/b
-basic/i
basic/c/d
basic/e/f
basic/g/h
+basic/i
basic/j/foo
basic/k/foo/bar
diff --git a/tests/common/empty.sh b/tests/common/empty.sh
new file mode 100644
index 0000000..95ee988
--- /dev/null
+++ b/tests/common/empty.sh
@@ -0,0 +1 @@
+bfs_diff basic -empty
diff --git a/tests/common/empty_error.out b/tests/common/empty_error.out
new file mode 100644
index 0000000..da45e23
--- /dev/null
+++ b/tests/common/empty_error.out
@@ -0,0 +1,3 @@
+./bar
+./baz
+./qux
diff --git a/tests/common/empty_error.sh b/tests/common/empty_error.sh
new file mode 100644
index 0000000..7c8049c
--- /dev/null
+++ b/tests/common/empty_error.sh
@@ -0,0 +1,7 @@
+cd "$TEST"
+
+"$XTOUCH" -p foo/ bar/ baz qux
+chmod -r foo baz
+defer chmod +r foo baz
+
+! bfs_diff . -empty
diff --git a/tests/common/empty_special.out b/tests/common/empty_special.out
new file mode 100644
index 0000000..fa35478
--- /dev/null
+++ b/tests/common/empty_special.out
@@ -0,0 +1,20 @@
+rainbow//
+rainbow/exec.sh
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ow
+rainbow/sgid
+rainbow/sticky
+rainbow/sticky_ow
+rainbow/sugid
+rainbow/suid
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/common/empty_special.sh b/tests/common/empty_special.sh
new file mode 100644
index 0000000..31e9d2e
--- /dev/null
+++ b/tests/common/empty_special.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -empty
diff --git a/tests/test_exec_substring.out b/tests/common/exec_substring.out
index 1c58fb2..32a6353 100644
--- a/tests/test_exec_substring.out
+++ b/tests/common/exec_substring.out
@@ -2,18 +2,18 @@
-basic/a-
-basic/b-
-basic/c-
+-basic/c/d-
-basic/e-
+-basic/e/f-
-basic/g-
+-basic/g/h-
-basic/i-
-basic/j-
--basic/k-
--basic/l-
--basic/c/d-
--basic/e/f-
--basic/g/h-
-basic/j/foo-
+-basic/k-
-basic/k/foo-
--basic/l/foo-
-basic/k/foo/bar-
+-basic/l-
+-basic/l/foo-
-basic/l/foo/bar-
-basic/l/foo/bar/baz-
diff --git a/tests/common/exec_substring.sh b/tests/common/exec_substring.sh
new file mode 100644
index 0000000..5cf8e85
--- /dev/null
+++ b/tests/common/exec_substring.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec echo '-{}-' \;
diff --git a/tests/common/execdir_nonexistent.out b/tests/common/execdir_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/common/execdir_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/common/execdir_nonexistent.sh b/tests/common/execdir_nonexistent.sh
new file mode 100644
index 0000000..0ec013c
--- /dev/null
+++ b/tests/common/execdir_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -print -execdir "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/test_execdir_pwd.out b/tests/common/execdir_pwd.out
index e66db9a..a11cd25 100644
--- a/tests/test_execdir_pwd.out
+++ b/tests/common/execdir_pwd.out
@@ -13,7 +13,7 @@ basic/e
basic/g
basic/j
basic/k
-basic/l
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/common/execdir_pwd.sh b/tests/common/execdir_pwd.sh
new file mode 100644
index 0000000..1c0165a
--- /dev/null
+++ b/tests/common/execdir_pwd.sh
@@ -0,0 +1,3 @@
+TMP_REAL=$(cd "$TMP" && pwd)
+OFFSET=$((${#TMP_REAL} + 2))
+bfs_diff basic -execdir bash -c "pwd | cut -b$OFFSET-" \;
diff --git a/tests/test_execdir_slash.out b/tests/common/execdir_slash.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slash.out
+++ b/tests/common/execdir_slash.out
diff --git a/tests/common/execdir_slash.sh b/tests/common/execdir_slash.sh
new file mode 100644
index 0000000..965f679
--- /dev/null
+++ b/tests/common/execdir_slash.sh
@@ -0,0 +1,2 @@
+# Don't prepend ./ for absolute paths in -execdir
+bfs_diff / -maxdepth 0 -execdir echo {} \;
diff --git a/tests/test_execdir_slash_pwd.out b/tests/common/execdir_slash_pwd.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slash_pwd.out
+++ b/tests/common/execdir_slash_pwd.out
diff --git a/tests/common/execdir_slash_pwd.sh b/tests/common/execdir_slash_pwd.sh
new file mode 100644
index 0000000..9c82e09
--- /dev/null
+++ b/tests/common/execdir_slash_pwd.sh
@@ -0,0 +1 @@
+bfs_diff / -maxdepth 0 -execdir pwd \;
diff --git a/tests/test_execdir_slashes.out b/tests/common/execdir_slashes.out
index b498fd4..b498fd4 100644
--- a/tests/test_execdir_slashes.out
+++ b/tests/common/execdir_slashes.out
diff --git a/tests/common/execdir_slashes.sh b/tests/common/execdir_slashes.sh
new file mode 100644
index 0000000..4e2b327
--- /dev/null
+++ b/tests/common/execdir_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -maxdepth 0 -execdir echo {} \;
diff --git a/tests/test_execdir_ulimit.out b/tests/common/execdir_ulimit.out
index 7f53982..bf52c09 100644
--- a/tests/test_execdir_ulimit.out
+++ b/tests/common/execdir_ulimit.out
@@ -1,3 +1,4 @@
+./.
./0
./1
./2
@@ -30,7 +31,6 @@
./q
./r
./s
-./scratch
./t
./u
./v
diff --git a/tests/common/execdir_ulimit.sh b/tests/common/execdir_ulimit.sh
new file mode 100644
index 0000000..122c282
--- /dev/null
+++ b/tests/common/execdir_ulimit.sh
@@ -0,0 +1,6 @@
+cd "$TEST"
+mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z
+mkdir -p a/b/c/d/e/f/g/h/i/j/k/l/m/0/1/2/3/4/5/6/7/8/9/A/B/C
+
+ulimit -n $((NOPENFD + 10))
+bfs_diff . -execdir echo {} \;
diff --git a/tests/test_flag_double_dash.out b/tests/common/flag_double_dash.out
index 774cc7c..774cc7c 100644
--- a/tests/test_flag_double_dash.out
+++ b/tests/common/flag_double_dash.out
diff --git a/tests/common/flag_double_dash.sh b/tests/common/flag_double_dash.sh
new file mode 100644
index 0000000..1075b06
--- /dev/null
+++ b/tests/common/flag_double_dash.sh
@@ -0,0 +1,2 @@
+cd basic
+bfs_diff -L -- . -type f
diff --git a/tests/test_L.out b/tests/common/follow.out
index e24f4f7..ec9e861 100644
--- a/tests/test_L.out
+++ b/tests/common/follow.out
@@ -1,17 +1,17 @@
links
links/broken
links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
links/file
links/hardlink
links/notdir
links/skip
-links/symlink
-links/deeply/nested
links/skip/broken
links/skip/dir
links/skip/file
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/dir
-links/deeply/nested/file
-links/deeply/nested/link
+links/symlink
diff --git a/tests/common/follow.sh b/tests/common/follow.sh
new file mode 100644
index 0000000..b5a2ae1
--- /dev/null
+++ b/tests/common/follow.sh
@@ -0,0 +1 @@
+bfs_diff links -follow
diff --git a/tests/test_lname.out b/tests/common/ilname.out
index e69de29..e69de29 100644
--- a/tests/test_lname.out
+++ b/tests/common/ilname.out
diff --git a/tests/common/ilname.sh b/tests/common/ilname.sh
new file mode 100644
index 0000000..fc7e9e4
--- /dev/null
+++ b/tests/common/ilname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ilname PATTERN || skip
+bfs_diff links -ilname '[AQ]'
diff --git a/tests/test_iname.out b/tests/common/iname.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_iname.out
+++ b/tests/common/iname.out
diff --git a/tests/common/iname.sh b/tests/common/iname.sh
new file mode 100644
index 0000000..c25a646
--- /dev/null
+++ b/tests/common/iname.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -iname PATTERN || skip
+bfs_diff basic -iname '*F*'
diff --git a/tests/test_inum.out b/tests/common/inum.out
index ebcaf79..ebcaf79 100644
--- a/tests/test_inum.out
+++ b/tests/common/inum.out
diff --git a/tests/common/inum.sh b/tests/common/inum.sh
new file mode 100644
index 0000000..ca63bbb
--- /dev/null
+++ b/tests/common/inum.sh
@@ -0,0 +1 @@
+bfs_diff basic -inum "$(inum basic/k/foo/bar)"
diff --git a/tests/common/inum_bind_mount.out b/tests/common/inum_bind_mount.out
new file mode 100644
index 0000000..ede8749
--- /dev/null
+++ b/tests/common/inum_bind_mount.out
@@ -0,0 +1,2 @@
+./bar
+./foo
diff --git a/tests/common/inum_bind_mount.sh b/tests/common/inum_bind_mount.sh
new file mode 100644
index 0000000..892713e
--- /dev/null
+++ b/tests/common/inum_bind_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" foo bar baz
+
+bfs_sudo mount --bind foo bar || skip
+defer bfs_sudo umount bar
+
+bfs_diff . -inum "$(inum bar)"
diff --git a/tests/common/inum_mount.out b/tests/common/inum_mount.out
new file mode 100644
index 0000000..99fa01e
--- /dev/null
+++ b/tests/common/inum_mount.out
@@ -0,0 +1 @@
+./mnt
diff --git a/tests/common/inum_mount.sh b/tests/common/inum_mount.sh
new file mode 100644
index 0000000..7facf57
--- /dev/null
+++ b/tests/common/inum_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_diff . -inum "$(inum mnt)"
diff --git a/tests/test_path.out b/tests/common/ipath.out
index 0d36df9..ae1ae21 100644
--- a/tests/test_path.out
+++ b/tests/common/ipath.out
@@ -1,7 +1,7 @@
basic/e/f
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/ipath.sh b/tests/common/ipath.sh
new file mode 100644
index 0000000..7d05f31
--- /dev/null
+++ b/tests/common/ipath.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -ipath PATTERN || skip
+bfs_diff basic -ipath 'basic/*F*'
diff --git a/tests/test_iregex.out b/tests/common/iregex.out
index cfc113b..cfc113b 100644
--- a/tests/test_iregex.out
+++ b/tests/common/iregex.out
diff --git a/tests/common/iregex.sh b/tests/common/iregex.sh
new file mode 100644
index 0000000..fc782f5
--- /dev/null
+++ b/tests/common/iregex.sh
@@ -0,0 +1 @@
+bfs_diff basic -iregex 'basic/[A-Z]/[a-z]'
diff --git a/tests/test_nogroup.out b/tests/common/lname.out
index e69de29..e69de29 100644
--- a/tests/test_nogroup.out
+++ b/tests/common/lname.out
diff --git a/tests/common/lname.sh b/tests/common/lname.sh
new file mode 100644
index 0000000..cf8a2a1
--- /dev/null
+++ b/tests/common/lname.sh
@@ -0,0 +1 @@
+bfs_diff links -lname '[aq]'
diff --git a/tests/common/ls.sh b/tests/common/ls.sh
new file mode 100644
index 0000000..bc50d90
--- /dev/null
+++ b/tests/common/ls.sh
@@ -0,0 +1 @@
+invoke_bfs rainbow -ls >"$OUT"
diff --git a/tests/test_maxdepth.out b/tests/common/maxdepth.out
index 7575ae4..7575ae4 100644
--- a/tests/test_maxdepth.out
+++ b/tests/common/maxdepth.out
diff --git a/tests/common/maxdepth.sh b/tests/common/maxdepth.sh
new file mode 100644
index 0000000..bb47cc9
--- /dev/null
+++ b/tests/common/maxdepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 1
diff --git a/tests/common/maxdepth_incomplete.sh b/tests/common/maxdepth_incomplete.sh
new file mode 100644
index 0000000..0bcb461
--- /dev/null
+++ b/tests/common/maxdepth_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -maxdepth
diff --git a/tests/test_mindepth.out b/tests/common/mindepth.out
index 1f00c58..3b461cf 100644
--- a/tests/test_mindepth.out
+++ b/tests/common/mindepth.out
@@ -1,18 +1,18 @@
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/common/mindepth.sh b/tests/common/mindepth.sh
new file mode 100644
index 0000000..22d7770
--- /dev/null
+++ b/tests/common/mindepth.sh
@@ -0,0 +1 @@
+bfs_diff basic -mindepth 1
diff --git a/tests/common/mindepth_incomplete.sh b/tests/common/mindepth_incomplete.sh
new file mode 100644
index 0000000..6f55a42
--- /dev/null
+++ b/tests/common/mindepth_incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -mindepth
diff --git a/tests/common/mount.out b/tests/common/mount.out
new file mode 100644
index 0000000..6253434
--- /dev/null
+++ b/tests/common/mount.out
@@ -0,0 +1,4 @@
+.
+./foo
+./foo/bar
+./mnt
diff --git a/tests/common/mount.sh b/tests/common/mount.sh
new file mode 100644
index 0000000..c9abde5
--- /dev/null
+++ b/tests/common/mount.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+"$XTOUCH" foo/bar mnt/baz
+
+bfs_diff . -mount
diff --git a/tests/test_name_slash.out b/tests/common/name_slash.out
index b498fd4..b498fd4 100644
--- a/tests/test_name_slash.out
+++ b/tests/common/name_slash.out
diff --git a/tests/common/name_slash.sh b/tests/common/name_slash.sh
new file mode 100644
index 0000000..8d89623
--- /dev/null
+++ b/tests/common/name_slash.sh
@@ -0,0 +1 @@
+bfs_diff / -maxdepth 0 -name /
diff --git a/tests/test_name_slashes.out b/tests/common/name_slashes.out
index 187b81f..187b81f 100644
--- a/tests/test_name_slashes.out
+++ b/tests/common/name_slashes.out
diff --git a/tests/common/name_slashes.sh b/tests/common/name_slashes.sh
new file mode 100644
index 0000000..78d0a84
--- /dev/null
+++ b/tests/common/name_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -maxdepth 0 -name /
diff --git a/tests/test_newer.out b/tests/common/newerma.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_newer.out
+++ b/tests/common/newerma.out
diff --git a/tests/common/newerma.sh b/tests/common/newerma.sh
new file mode 100644
index 0000000..b05af8d
--- /dev/null
+++ b/tests/common/newerma.sh
@@ -0,0 +1 @@
+bfs_diff times -newerma times/a
diff --git a/tests/test_newermt.out b/tests/common/newermt.out
index 650e550..650e550 100644
--- a/tests/test_newermt.out
+++ b/tests/common/newermt.out
diff --git a/tests/common/newermt.sh b/tests/common/newermt.sh
new file mode 100644
index 0000000..e816b29
--- /dev/null
+++ b/tests/common/newermt.sh
@@ -0,0 +1,3 @@
+bfs_diff times -newermt 1991-12-14T00:01 \
+ -newermt "1991-12-14 01:01+01:00" \
+ -newermt "19911213 20:31:00-0330"
diff --git a/tests/test_newermt_epoch_minus_one.out b/tests/common/newermt_epoch_minus_one.out
index f7f63b0..f7f63b0 100644
--- a/tests/test_newermt_epoch_minus_one.out
+++ b/tests/common/newermt_epoch_minus_one.out
diff --git a/tests/common/newermt_epoch_minus_one.sh b/tests/common/newermt_epoch_minus_one.sh
new file mode 100644
index 0000000..568e2f3
--- /dev/null
+++ b/tests/common/newermt_epoch_minus_one.sh
@@ -0,0 +1 @@
+bfs_diff times -newermt 1969-12-31T23:59:59Z
diff --git a/tests/test_nogroup_ulimit.out b/tests/common/ok_closed_stdin.out
index e69de29..e69de29 100644
--- a/tests/test_nogroup_ulimit.out
+++ b/tests/common/ok_closed_stdin.out
diff --git a/tests/common/ok_closed_stdin.sh b/tests/common/ok_closed_stdin.sh
new file mode 100644
index 0000000..687e998
--- /dev/null
+++ b/tests/common/ok_closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic -ok echo \; <&-
diff --git a/tests/test_nouser.out b/tests/common/okdir_closed_stdin.out
index e69de29..e69de29 100644
--- a/tests/test_nouser.out
+++ b/tests/common/okdir_closed_stdin.out
diff --git a/tests/common/okdir_closed_stdin.sh b/tests/common/okdir_closed_stdin.sh
new file mode 100644
index 0000000..a515298
--- /dev/null
+++ b/tests/common/okdir_closed_stdin.sh
@@ -0,0 +1 @@
+bfs_diff basic -okdir echo {} \; <&-
diff --git a/tests/test_name_root_depth.out b/tests/common/quit.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_name_root_depth.out
+++ b/tests/common/quit.out
diff --git a/tests/common/quit.sh b/tests/common/quit.sh
new file mode 100644
index 0000000..46b60c5
--- /dev/null
+++ b/tests/common/quit.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -print -name g -quit
diff --git a/tests/test_comma_reachability.out b/tests/common/quit_after_print.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_reachability.out
+++ b/tests/common/quit_after_print.out
diff --git a/tests/common/quit_after_print.sh b/tests/common/quit_after_print.sh
new file mode 100644
index 0000000..ee5653a
--- /dev/null
+++ b/tests/common/quit_after_print.sh
@@ -0,0 +1 @@
+bfs_diff basic basic -print -quit
diff --git a/tests/test_nouser_ulimit.out b/tests/common/quit_before_print.out
index e69de29..e69de29 100644
--- a/tests/test_nouser_ulimit.out
+++ b/tests/common/quit_before_print.out
diff --git a/tests/common/quit_before_print.sh b/tests/common/quit_before_print.sh
new file mode 100644
index 0000000..cda3a2c
--- /dev/null
+++ b/tests/common/quit_before_print.sh
@@ -0,0 +1 @@
+bfs_diff basic basic -quit -print
diff --git a/tests/test_quit_child.out b/tests/common/quit_child.out
index fb683c7..fb683c7 100644
--- a/tests/test_quit_child.out
+++ b/tests/common/quit_child.out
diff --git a/tests/common/quit_child.sh b/tests/common/quit_child.sh
new file mode 100644
index 0000000..bd27eff
--- /dev/null
+++ b/tests/common/quit_child.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -print -name h -quit
diff --git a/tests/test_quit_depth.out b/tests/common/quit_depth.out
index fb683c7..fb683c7 100644
--- a/tests/test_quit_depth.out
+++ b/tests/common/quit_depth.out
diff --git a/tests/common/quit_depth.sh b/tests/common/quit_depth.sh
new file mode 100644
index 0000000..f5f82ba
--- /dev/null
+++ b/tests/common/quit_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -print -name g -quit
diff --git a/tests/test_quit_depth_child.out b/tests/common/quit_depth_child.out
index 72b086d..72b086d 100644
--- a/tests/test_quit_depth_child.out
+++ b/tests/common/quit_depth_child.out
diff --git a/tests/common/quit_depth_child.sh b/tests/common/quit_depth_child.sh
new file mode 100644
index 0000000..dd09d5b
--- /dev/null
+++ b/tests/common/quit_depth_child.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -print -name h -quit
diff --git a/tests/test_regex.out b/tests/common/regex.out
index cfc113b..cfc113b 100644
--- a/tests/test_regex.out
+++ b/tests/common/regex.out
diff --git a/tests/common/regex.sh b/tests/common/regex.sh
new file mode 100644
index 0000000..a3bdae8
--- /dev/null
+++ b/tests/common/regex.sh
@@ -0,0 +1 @@
+bfs_diff basic -regex 'basic/./.'
diff --git a/tests/test_regex_parens.out b/tests/common/regex_parens.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regex_parens.out
+++ b/tests/common/regex_parens.out
diff --git a/tests/common/regex_parens.sh b/tests/common/regex_parens.sh
new file mode 100644
index 0000000..fe0abf6
--- /dev/null
+++ b/tests/common/regex_parens.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff . -regex '\./\((\)'
diff --git a/tests/test_links.out b/tests/common/samefile.out
index 996ffc8..996ffc8 100644
--- a/tests/test_links.out
+++ b/tests/common/samefile.out
diff --git a/tests/common/samefile.sh b/tests/common/samefile.sh
new file mode 100644
index 0000000..8e51966
--- /dev/null
+++ b/tests/common/samefile.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/file
diff --git a/tests/test_L_broken.out b/tests/common/samefile_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_L_broken.out
+++ b/tests/common/samefile_broken.out
diff --git a/tests/common/samefile_broken.sh b/tests/common/samefile_broken.sh
new file mode 100644
index 0000000..1cb52db
--- /dev/null
+++ b/tests/common/samefile_broken.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/broken
diff --git a/tests/test_L_notdir.out b/tests/common/samefile_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_L_notdir.out
+++ b/tests/common/samefile_notdir.out
diff --git a/tests/common/samefile_notdir.sh b/tests/common/samefile_notdir.sh
new file mode 100644
index 0000000..f274ef6
--- /dev/null
+++ b/tests/common/samefile_notdir.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/notdir
diff --git a/tests/test_samefile_symlink.out b/tests/common/samefile_symlink.out
index 299a572..299a572 100644
--- a/tests/test_samefile_symlink.out
+++ b/tests/common/samefile_symlink.out
diff --git a/tests/common/samefile_symlink.sh b/tests/common/samefile_symlink.sh
new file mode 100644
index 0000000..55ccf5c
--- /dev/null
+++ b/tests/common/samefile_symlink.sh
@@ -0,0 +1 @@
+bfs_diff links -samefile links/symlink
diff --git a/tests/common/samefile_wordesc.sh b/tests/common/samefile_wordesc.sh
new file mode 100644
index 0000000..b5d158f
--- /dev/null
+++ b/tests/common/samefile_wordesc.sh
@@ -0,0 +1,4 @@
+# Regression test: don't abort on incomplete UTF-8 sequences
+export LC_ALL=$(locale -a | grep -Ei 'utf-?8$' | head -n1)
+test -n "$LC_ALL" || skip
+! invoke_bfs -samefile $'\xFA\xFA'
diff --git a/tests/test_ok_closed_stdin.out b/tests/common/size_big.out
index e69de29..e69de29 100644
--- a/tests/test_ok_closed_stdin.out
+++ b/tests/common/size_big.out
diff --git a/tests/common/size_big.sh b/tests/common/size_big.sh
new file mode 100644
index 0000000..6c100eb
--- /dev/null
+++ b/tests/common/size_big.sh
@@ -0,0 +1 @@
+bfs_diff basic -size 9223372036854775807
diff --git a/tests/find-color.sh b/tests/find-color.sh
index ecdd5af..47de2a2 100755
--- a/tests/find-color.sh
+++ b/tests/find-color.sh
@@ -1,20 +1,7 @@
#!/usr/bin/env bash
-############################################################################
-# bfs #
-# Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
set -e
diff --git a/tests/getopts.sh b/tests/getopts.sh
new file mode 100644
index 0000000..5214e9f
--- /dev/null
+++ b/tests/getopts.sh
@@ -0,0 +1,179 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Argument parsing
+
+if command -v nproc &>/dev/null; then
+ JOBS=$(nproc)
+else
+ JOBS=1
+fi
+MAKE=
+PATTERNS=()
+SUDO=()
+STOP=0
+CLEAN=1
+UPDATE=0
+VERBOSE_COMMANDS=0
+VERBOSE_ERRORS=0
+VERBOSE_SKIPPED=0
+VERBOSE_TESTS=0
+
+# Print usage information
+usage() {
+ local pad=$(printf "%*s" ${#0} "")
+ color cat <<EOF
+Usage: ${GRN}$0${RST}
+ [${BLU}-j${RST}${BLD}N${RST}] [${BLU}--make${RST}=${BLD}MAKE${RST}] [${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}] [${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]]
+ [${BLU}--stop${RST}] [${BLU}--no-clean${RST}] [${BLU}--update${RST}] [${BLU}--verbose${RST}[=${BLD}LEVEL${RST}]] [${BLU}--help${RST}]
+ [${BLU}--posix${RST}] [${BLU}--bsd${RST}] [${BLU}--gnu${RST}] [${BLU}--all${RST}] [${BLD}TEST${RST} [${BLD}TEST${RST} ...]]
+
+ ${BLU}-j${RST}${BLD}N${RST}
+ Run ${BLD}N${RST} tests in parallel (default: ${BLD}$JOBS${RST})
+
+ ${BLU}--make${RST}=${BLD}MAKE${RST}
+ Use the jobserver from ${BLD}MAKE${RST}, e.g. ${BLU}--make${RST}=${BLD}"make -j$JOBS"${RST}
+
+ ${BLU}--bfs${RST}=${BLD}path/to/bfs${RST}
+ Set the path to the bfs executable to test (default: ${BLD}./bin/bfs${RST})
+
+ ${BLU}--sudo${RST}[=${BLD}COMMAND${RST}]
+ Run tests that require root using ${GRN}sudo${RST} or the given ${BLD}COMMAND${RST}
+
+ ${BLU}--stop${RST}
+ Stop when the first error occurs
+
+ ${BLU}--no-clean${RST}
+ Keep the test directories around after the run
+
+ ${BLU}--update${RST}
+ Update the expected outputs for the test cases
+
+ ${BLU}--verbose${RST}=${BLD}commands${RST}
+ Log the commands that get executed
+ ${BLU}--verbose${RST}=${BLD}errors${RST}
+ Don't redirect standard error
+ ${BLU}--verbose${RST}=${BLD}skipped${RST}
+ Log which tests get skipped
+ ${BLU}--verbose${RST}=${BLD}tests${RST}
+ Log all tests that get run
+ ${BLU}--verbose${RST}
+ Log everything
+
+ ${BLU}--help${RST}
+ This message
+
+ ${BLU}--posix${RST}, ${BLU}--bsd${RST}, ${BLU}--gnu${RST}, ${BLU}--all${RST}
+ Choose which test cases to run (default: ${BLU}--all${RST})
+
+ ${BLD}TEST${RST}
+ Select individual test cases to run (e.g. ${BLD}posix/basic${RST}, ${BLD}"*exec*"${RST}, ...)
+EOF
+}
+
+# Parse the command line
+parse_args() {
+ for arg; do
+ case "$arg" in
+ -j?*)
+ JOBS="${arg#-j}"
+ ;;
+ --make=*)
+ MAKE="${arg#*=}"
+ ;;
+ --bfs=*)
+ BFS="${arg#*=}"
+ ;;
+ --posix)
+ PATTERNS+=("posix/*")
+ ;;
+ --bsd)
+ PATTERNS+=("posix/*" "common/*" "bsd/*")
+ ;;
+ --gnu)
+ PATTERNS+=("posix/*" "common/*" "gnu/*")
+ ;;
+ --all)
+ PATTERNS+=("*")
+ ;;
+ --sudo)
+ SUDO=(sudo)
+ ;;
+ --sudo=*)
+ read -a SUDO <<<"${arg#*=}"
+ ;;
+ --stop)
+ STOP=1
+ ;;
+ --no-clean|--noclean)
+ CLEAN=0
+ ;;
+ --update)
+ UPDATE=1
+ ;;
+ --verbose=commands)
+ VERBOSE_COMMANDS=1
+ ;;
+ --verbose=errors)
+ VERBOSE_ERRORS=1
+ ;;
+ --verbose=skipped)
+ VERBOSE_SKIPPED=1
+ ;;
+ --verbose=tests)
+ VERBOSE_TESTS=1
+ ;;
+ --verbose)
+ VERBOSE_COMMANDS=1
+ VERBOSE_ERRORS=1
+ VERBOSE_SKIPPED=1
+ VERBOSE_TESTS=1
+ ;;
+ --help)
+ usage
+ exit 0
+ ;;
+ -*)
+ color printf "${RED}error:${RST} Unrecognized option '%s'.\n\n" "$arg" >&2
+ usage >&2
+ exit 1
+ ;;
+ *)
+ PATTERNS+=("$arg")
+ ;;
+ esac
+ done
+
+ read -a MAKE <<<"$MAKE"
+
+ # Try to resolve the path to $BFS before we cd, while also supporting
+ # --bfs="./bin/bfs -S ids"
+ read -a BFS <<<"${BFS:-$BIN/bfs}"
+ BFS[0]=$(_realpath "$(command -v "${BFS[0]}")")
+
+ if ((${#PATTERNS[@]} == 0)); then
+ PATTERNS=("*")
+ fi
+
+ TEST_CASES=()
+ ALL_TESTS=($(cd "$TESTS" && quote {posix,common,bsd,gnu,bfs}/*.sh))
+ for TEST in "${ALL_TESTS[@]}"; do
+ TEST="${TEST%.sh}"
+ for PATTERN in "${PATTERNS[@]}"; do
+ if [[ $TEST == $PATTERN ]]; then
+ TEST_CASES+=("$TEST")
+ break
+ fi
+ done
+ done
+
+ if ((${#TEST_CASES[@]} == 0)); then
+ color printf "${RED}error:${RST} No tests matched" >&2
+ color printf " ${BLD}%s${RST}" "${PATTERNS[@]}" >&2
+ color printf ".\n\n" >&2
+ usage >&2
+ exit 1
+ fi
+}
diff --git a/tests/gnu/L_delete.out b/tests/gnu/L_delete.out
new file mode 100644
index 0000000..7ed5f0d
--- /dev/null
+++ b/tests/gnu/L_delete.out
@@ -0,0 +1,2 @@
+.
+./foo
diff --git a/tests/gnu/L_delete.sh b/tests/gnu/L_delete.sh
new file mode 100644
index 0000000..0559c49
--- /dev/null
+++ b/tests/gnu/L_delete.sh
@@ -0,0 +1,8 @@
+cd "$TEST"
+mkdir foo bar
+ln -s ../foo bar/baz
+
+# Don't try to rmdir() a symlink
+invoke_bfs -L bar -delete
+
+bfs_diff .
diff --git a/tests/test_L_loops_continue.out b/tests/gnu/L_loops_continue.out
index fbe0cac..a514555 100644
--- a/tests/test_L_loops_continue.out
+++ b/tests/gnu/L_loops_continue.out
@@ -1,11 +1,11 @@
loops
loops/broken
loops/deeply
+loops/deeply/nested
+loops/deeply/nested/dir
loops/file
loops/notdir
loops/skip
-loops/symlink
-loops/deeply/nested
loops/skip/dir
loops/skip/loop
-loops/deeply/nested/dir
+loops/symlink
diff --git a/tests/gnu/L_loops_continue.sh b/tests/gnu/L_loops_continue.sh
new file mode 100644
index 0000000..55aeb33
--- /dev/null
+++ b/tests/gnu/L_loops_continue.sh
@@ -0,0 +1 @@
+! bfs_diff -L loops
diff --git a/tests/test_L_xtype_f.out b/tests/gnu/L_xtype_f.out
index 78953d1..8b95397 100644
--- a/tests/test_L_xtype_f.out
+++ b/tests/gnu/L_xtype_f.out
@@ -1,4 +1,4 @@
+links/deeply/nested/file
links/file
links/hardlink
links/skip/file
-links/deeply/nested/file
diff --git a/tests/gnu/L_xtype_f.sh b/tests/gnu/L_xtype_f.sh
new file mode 100644
index 0000000..47f7be7
--- /dev/null
+++ b/tests/gnu/L_xtype_f.sh
@@ -0,0 +1 @@
+bfs_diff -L links -xtype f
diff --git a/tests/test_L_xtype_l.out b/tests/gnu/L_xtype_l.out
index 93cfc26..973864f 100644
--- a/tests/test_L_xtype_l.out
+++ b/tests/gnu/L_xtype_l.out
@@ -1,8 +1,8 @@
links/broken
+links/deeply/nested/broken
+links/deeply/nested/link
links/notdir
links/skip
-links/symlink
links/skip/broken
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/link
+links/symlink
diff --git a/tests/gnu/L_xtype_l.sh b/tests/gnu/L_xtype_l.sh
new file mode 100644
index 0000000..afe52ef
--- /dev/null
+++ b/tests/gnu/L_xtype_l.sh
@@ -0,0 +1 @@
+bfs_diff -L links -xtype l
diff --git a/tests/test_a.out b/tests/gnu/and.out
index 722962c..722962c 100644
--- a/tests/test_a.out
+++ b/tests/gnu/and.out
diff --git a/tests/gnu/and.sh b/tests/gnu/and.sh
new file mode 100644
index 0000000..1606455
--- /dev/null
+++ b/tests/gnu/and.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -and -type d
diff --git a/tests/test_comma_redundant_false.out b/tests/gnu/and_false_or_true.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_redundant_false.out
+++ b/tests/gnu/and_false_or_true.out
diff --git a/tests/gnu/and_false_or_true.sh b/tests/gnu/and_false_or_true.sh
new file mode 100644
index 0000000..e500722
--- /dev/null
+++ b/tests/gnu/and_false_or_true.sh
@@ -0,0 +1,3 @@
+# Test (-a lhs(always_true) -false) <==> (! lhs),
+# (-o lhs(always_false) -true) <==> (! lhs)
+bfs_diff basic -prune -false -o -true
diff --git a/tests/test_okdir_closed_stdin.out b/tests/gnu/and_purity.out
index e69de29..e69de29 100644
--- a/tests/test_okdir_closed_stdin.out
+++ b/tests/gnu/and_purity.out
diff --git a/tests/gnu/and_purity.sh b/tests/gnu/and_purity.sh
new file mode 100644
index 0000000..55e2cfc
--- /dev/null
+++ b/tests/gnu/and_purity.sh
@@ -0,0 +1,2 @@
+# Regression test: (-a lhs(pure) rhs(always_false)) <==> rhs is only valid if rhs is pure
+bfs_diff basic -name nonexistent \( -print , -false \)
diff --git a/tests/test_comma.out b/tests/gnu/comma.out
index 56b2bde..740eefc 100644
--- a/tests/test_comma.out
+++ b/tests/gnu/comma.out
@@ -2,22 +2,22 @@ basic
basic/a
basic/b
basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
basic/c/d
+basic/e
basic/e/f
basic/e/f
+basic/g
basic/g/h
+basic/i
+basic/j
basic/j/foo
basic/j/foo
+basic/k
basic/k/foo
basic/k/foo
+basic/k/foo/bar
+basic/l
basic/l/foo
basic/l/foo
-basic/k/foo/bar
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/gnu/comma.sh b/tests/gnu/comma.sh
new file mode 100644
index 0000000..cdcebf8
--- /dev/null
+++ b/tests/gnu/comma.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '*f*' -print , -print
diff --git a/tests/test_comma_redundant_true.out b/tests/gnu/comma_reachability.out
index 15a13db..15a13db 100644
--- a/tests/test_comma_redundant_true.out
+++ b/tests/gnu/comma_reachability.out
diff --git a/tests/gnu/comma_reachability.sh b/tests/gnu/comma_reachability.sh
new file mode 100644
index 0000000..60c26bc
--- /dev/null
+++ b/tests/gnu/comma_reachability.sh
@@ -0,0 +1 @@
+bfs_diff basic -print -quit , -print
diff --git a/tests/test_fprint_truncate.out b/tests/gnu/comma_redundant_false.out
index 15a13db..15a13db 100644
--- a/tests/test_fprint_truncate.out
+++ b/tests/gnu/comma_redundant_false.out
diff --git a/tests/gnu/comma_redundant_false.sh b/tests/gnu/comma_redundant_false.sh
new file mode 100644
index 0000000..f35d9b8
--- /dev/null
+++ b/tests/gnu/comma_redundant_false.sh
@@ -0,0 +1,2 @@
+# Test (, lhs(always_false) -false) <==> lhs
+bfs_diff basic -print -not -prune , -false
diff --git a/tests/test_not_reachability.out b/tests/gnu/comma_redundant_true.out
index 15a13db..15a13db 100644
--- a/tests/test_not_reachability.out
+++ b/tests/gnu/comma_redundant_true.out
diff --git a/tests/gnu/comma_redundant_true.sh b/tests/gnu/comma_redundant_true.sh
new file mode 100644
index 0000000..f9eef57
--- /dev/null
+++ b/tests/gnu/comma_redundant_true.sh
@@ -0,0 +1,2 @@
+# Test (, lhs(always_true) -true) <==> lhs
+bfs_diff basic -prune , -true
diff --git a/tests/gnu/daystart.out b/tests/gnu/daystart.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/daystart.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/daystart.sh b/tests/gnu/daystart.sh
new file mode 100644
index 0000000..9c3be1a
--- /dev/null
+++ b/tests/gnu/daystart.sh
@@ -0,0 +1 @@
+TZ=WAT-1 bfs_diff basic -daystart -mtime 0
diff --git a/tests/gnu/daystart_twice.out b/tests/gnu/daystart_twice.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/daystart_twice.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/daystart_twice.sh b/tests/gnu/daystart_twice.sh
new file mode 100644
index 0000000..edbf18d
--- /dev/null
+++ b/tests/gnu/daystart_twice.sh
@@ -0,0 +1 @@
+TZ=WAT-1 bfs_diff basic -daystart -daystart -mtime 0
diff --git a/tests/gnu/exec_flush.out b/tests/gnu/exec_flush.out
new file mode 100644
index 0000000..fdd7b16
--- /dev/null
+++ b/tests/gnu/exec_flush.out
@@ -0,0 +1,19 @@
+basic found
+basic/a found
+basic/b found
+basic/c found
+basic/c/d found
+basic/e found
+basic/e/f found
+basic/g found
+basic/g/h found
+basic/i found
+basic/j found
+basic/j/foo found
+basic/k found
+basic/k/foo found
+basic/k/foo/bar found
+basic/l found
+basic/l/foo found
+basic/l/foo/bar found
+basic/l/foo/bar/baz found
diff --git a/tests/gnu/exec_flush.sh b/tests/gnu/exec_flush.sh
new file mode 100644
index 0000000..ff6088e
--- /dev/null
+++ b/tests/gnu/exec_flush.sh
@@ -0,0 +1,4 @@
+# I/O streams should be flushed before executing programs
+invoke_bfs basic -print0 -exec echo found \; | tr '\0' ' ' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/gnu/exec_flush_fail.sh b/tests/gnu/exec_flush_fail.sh
new file mode 100644
index 0000000..5505f7a
--- /dev/null
+++ b/tests/gnu/exec_flush_fail.sh
@@ -0,0 +1,3 @@
+# Failure to flush streams before exec should be caught
+test -e /dev/full || skip
+! invoke_bfs basic -print0 -exec true \; >/dev/full
diff --git a/tests/gnu/exec_nothing.sh b/tests/gnu/exec_nothing.sh
new file mode 100644
index 0000000..443aa0d
--- /dev/null
+++ b/tests/gnu/exec_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on missing command
+! invoke_bfs basic -exec \;
diff --git a/tests/gnu/exec_plus_flush.out b/tests/gnu/exec_plus_flush.out
new file mode 100644
index 0000000..3e276be
--- /dev/null
+++ b/tests/gnu/exec_plus_flush.out
Binary files differ
diff --git a/tests/gnu/exec_plus_flush.sh b/tests/gnu/exec_plus_flush.sh
new file mode 100644
index 0000000..0c03837
--- /dev/null
+++ b/tests/gnu/exec_plus_flush.sh
@@ -0,0 +1,2 @@
+invoke_bfs basic/a -print0 -exec echo found {} + >"$OUT"
+diff_output
diff --git a/tests/gnu/exec_plus_flush_fail.sh b/tests/gnu/exec_plus_flush_fail.sh
new file mode 100644
index 0000000..53a50e5
--- /dev/null
+++ b/tests/gnu/exec_plus_flush_fail.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic/a -print0 -exec echo found {} + >/dev/full
diff --git a/tests/test_execdir.out b/tests/gnu/execdir.out
index 62b31f6..62b31f6 100644
--- a/tests/test_execdir.out
+++ b/tests/gnu/execdir.out
diff --git a/tests/gnu/execdir.sh b/tests/gnu/execdir.sh
new file mode 100644
index 0000000..5a3a95a
--- /dev/null
+++ b/tests/gnu/execdir.sh
@@ -0,0 +1 @@
+bfs_diff basic -execdir echo {} \;
diff --git a/tests/gnu/execdir_path_dot.sh b/tests/gnu/execdir_path_dot.sh
new file mode 100644
index 0000000..632dbb4
--- /dev/null
+++ b/tests/gnu/execdir_path_dot.sh
@@ -0,0 +1 @@
+! PATH=".:$PATH" invoke_bfs basic -execdir echo {} +
diff --git a/tests/gnu/execdir_path_empty.sh b/tests/gnu/execdir_path_empty.sh
new file mode 100644
index 0000000..eda6b1c
--- /dev/null
+++ b/tests/gnu/execdir_path_empty.sh
@@ -0,0 +1 @@
+! PATH=":$PATH" invoke_bfs basic -execdir echo {} +
diff --git a/tests/gnu/execdir_path_relative.sh b/tests/gnu/execdir_path_relative.sh
new file mode 100644
index 0000000..69899ad
--- /dev/null
+++ b/tests/gnu/execdir_path_relative.sh
@@ -0,0 +1 @@
+! PATH="foo:$PATH" invoke_bfs basic -execdir echo {} +
diff --git a/tests/test_execdir_plus_semicolon.out b/tests/gnu/execdir_plus_semicolon.out
index e39f452..e39f452 100644
--- a/tests/test_execdir_plus_semicolon.out
+++ b/tests/gnu/execdir_plus_semicolon.out
diff --git a/tests/gnu/execdir_plus_semicolon.sh b/tests/gnu/execdir_plus_semicolon.sh
new file mode 100644
index 0000000..c5cdafe
--- /dev/null
+++ b/tests/gnu/execdir_plus_semicolon.sh
@@ -0,0 +1 @@
+bfs_diff basic -execdir echo foo {} bar + baz \;
diff --git a/tests/test_execdir_substring.out b/tests/gnu/execdir_substring.out
index f7a9ac0..f7a9ac0 100644
--- a/tests/test_execdir_substring.out
+++ b/tests/gnu/execdir_substring.out
diff --git a/tests/gnu/execdir_substring.sh b/tests/gnu/execdir_substring.sh
new file mode 100644
index 0000000..feeabc4
--- /dev/null
+++ b/tests/gnu/execdir_substring.sh
@@ -0,0 +1 @@
+bfs_diff basic -execdir echo '-{}-' \;
diff --git a/tests/gnu/execdir_ulimit.out b/tests/gnu/execdir_ulimit.out
new file mode 100644
index 0000000..6749f7d
--- /dev/null
+++ b/tests/gnu/execdir_ulimit.out
@@ -0,0 +1,16 @@
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 ./0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
diff --git a/tests/gnu/execdir_ulimit.sh b/tests/gnu/execdir_ulimit.sh
new file mode 100644
index 0000000..e14e716
--- /dev/null
+++ b/tests/gnu/execdir_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -Sn 64
+bfs_diff deep -type f -execdir bash -c 'printf "%d %s\n" $(ulimit -Sn) "$1"' bash {} \;
diff --git a/tests/test_executable.out b/tests/gnu/executable.out
index 49c1b21..49c1b21 100644
--- a/tests/test_executable.out
+++ b/tests/gnu/executable.out
diff --git a/tests/gnu/executable.sh b/tests/gnu/executable.sh
new file mode 100644
index 0000000..f7f6633
--- /dev/null
+++ b/tests/gnu/executable.sh
@@ -0,0 +1 @@
+bfs_diff perms -executable
diff --git a/tests/test_or_purity.out b/tests/gnu/false.out
index e69de29..e69de29 100644
--- a/tests/test_or_purity.out
+++ b/tests/gnu/false.out
diff --git a/tests/gnu/false.sh b/tests/gnu/false.sh
new file mode 100644
index 0000000..89d86c2
--- /dev/null
+++ b/tests/gnu/false.sh
@@ -0,0 +1 @@
+bfs_diff basic -false
diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh
new file mode 100644
index 0000000..85eee8f
--- /dev/null
+++ b/tests/gnu/files0_from_empty.sh
@@ -0,0 +1 @@
+! printf "\0" | invoke_bfs -files0-from -
diff --git a/tests/gnu/files0_from_error.sh b/tests/gnu/files0_from_error.sh
new file mode 100644
index 0000000..1515d0b
--- /dev/null
+++ b/tests/gnu/files0_from_error.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from basic
diff --git a/tests/test_files0_from_file.out b/tests/gnu/files0_from_file.out
index 3648854..1d87e6b 100644
--- a/tests/test_files0_from_file.out
+++ b/tests/gnu/files0_from_file.out
@@ -1,30 +1,33 @@
-!
-!-
-(
-(-
-)
-,
--
-...
-\
/j
/j
+!
+!-
!-/e
!-/e
!/d
!/d
+(
+(-
(-/c
(-/c
(/b
(/b
+)
)/g
)/g
+,
,/f
,/f
+-
-/a
-/a
+...
.../h
.../h
+[
+[/k
+[/k
+\
\/i
\/i
diff --git a/tests/gnu/files0_from_file.sh b/tests/gnu/files0_from_file.sh
new file mode 100644
index 0000000..81435a0
--- /dev/null
+++ b/tests/gnu/files0_from_file.sh
@@ -0,0 +1,4 @@
+FILE="$TMP/$TEST.in"
+cd weirdnames
+invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0"
+bfs_diff -files0-from "$FILE"
diff --git a/tests/test_perm_leading_plus_symbolic.out b/tests/gnu/files0_from_none.out
index e69de29..e69de29 100644
--- a/tests/test_perm_leading_plus_symbolic.out
+++ b/tests/gnu/files0_from_none.out
diff --git a/tests/gnu/files0_from_none.sh b/tests/gnu/files0_from_none.sh
new file mode 100644
index 0000000..1633163
--- /dev/null
+++ b/tests/gnu/files0_from_none.sh
@@ -0,0 +1 @@
+printf "" | bfs_diff -files0-from -
diff --git a/tests/gnu/files0_from_nothing.sh b/tests/gnu/files0_from_nothing.sh
new file mode 100644
index 0000000..fee50a8
--- /dev/null
+++ b/tests/gnu/files0_from_nothing.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from basic/nonexistent
diff --git a/tests/gnu/files0_from_nowhere.sh b/tests/gnu/files0_from_nowhere.sh
new file mode 100644
index 0000000..68eea4b
--- /dev/null
+++ b/tests/gnu/files0_from_nowhere.sh
@@ -0,0 +1 @@
+! invoke_bfs -files0-from
diff --git a/tests/gnu/files0_from_ok.sh b/tests/gnu/files0_from_ok.sh
new file mode 100644
index 0000000..8e145ce
--- /dev/null
+++ b/tests/gnu/files0_from_ok.sh
@@ -0,0 +1 @@
+! printf "basic\0" | invoke_bfs -files0-from - -ok echo {} \;
diff --git a/tests/test_files0_from_stdin.out b/tests/gnu/files0_from_stdin.out
index 3648854..1d87e6b 100644
--- a/tests/test_files0_from_stdin.out
+++ b/tests/gnu/files0_from_stdin.out
@@ -1,30 +1,33 @@
-!
-!-
-(
-(-
-)
-,
--
-...
-\
/j
/j
+!
+!-
!-/e
!-/e
!/d
!/d
+(
+(-
(-/c
(-/c
(/b
(/b
+)
)/g
)/g
+,
,/f
,/f
+-
-/a
-/a
+...
.../h
.../h
+[
+[/k
+[/k
+\
\/i
\/i
diff --git a/tests/gnu/files0_from_stdin.sh b/tests/gnu/files0_from_stdin.sh
new file mode 100644
index 0000000..9df7736
--- /dev/null
+++ b/tests/gnu/files0_from_stdin.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from -
diff --git a/tests/gnu/fls.sh b/tests/gnu/fls.sh
new file mode 100644
index 0000000..d2ff794
--- /dev/null
+++ b/tests/gnu/fls.sh
@@ -0,0 +1 @@
+invoke_bfs rainbow -fls "$OUT"
diff --git a/tests/gnu/fls_nonexistent.sh b/tests/gnu/fls_nonexistent.sh
new file mode 100644
index 0000000..2854569
--- /dev/null
+++ b/tests/gnu/fls_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs rainbow -fls nonexistent/path
diff --git a/tests/test_follow_comma.out b/tests/gnu/follow_comma.out
index 8b90e76..920b3d3 100644
--- a/tests/test_follow_comma.out
+++ b/tests/gnu/follow_comma.out
@@ -1,21 +1,23 @@
.
./
+./ /j
./!
./!-
-./(
-./(-
-./)
-./,
-./-
-./...
-./\
-./ /j
./!-/e
./!/d
+./(
+./(-
./(-/c
./(/b
+./)
./)/g
+./,
./,/f
+./-
./-/a
+./...
./.../h
+./[
+./[/k
+./\
./\/i
diff --git a/tests/gnu/follow_comma.sh b/tests/gnu/follow_comma.sh
new file mode 100644
index 0000000..f57b932
--- /dev/null
+++ b/tests/gnu/follow_comma.sh
@@ -0,0 +1,3 @@
+# , is an operator after a non-flag is seen
+cd weirdnames
+bfs_diff -follow ',' -print
diff --git a/tests/gnu/fprint.out b/tests/gnu/fprint.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/fprint.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/fprint.sh b/tests/gnu/fprint.sh
new file mode 100644
index 0000000..04b50fa
--- /dev/null
+++ b/tests/gnu/fprint.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprint "$OUT"
+sort_output
+diff_output
diff --git a/tests/test_fprint0.out b/tests/gnu/fprint0.out
index 1347444..1347444 100644
--- a/tests/test_fprint0.out
+++ b/tests/gnu/fprint0.out
Binary files differ
diff --git a/tests/gnu/fprint0.sh b/tests/gnu/fprint0.sh
new file mode 100644
index 0000000..dd10b5f
--- /dev/null
+++ b/tests/gnu/fprint0.sh
@@ -0,0 +1,2 @@
+invoke_bfs basic/a basic/b -fprint0 "$OUT"
+diff_output
diff --git a/tests/gnu/fprint0_nonexistent.sh b/tests/gnu/fprint0_nonexistent.sh
new file mode 100644
index 0000000..4906081
--- /dev/null
+++ b/tests/gnu/fprint0_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprint0 nonexistent/path
diff --git a/tests/test_fprint_duplicate.out b/tests/gnu/fprint_duplicate.out
index 2575f35..2575f35 100644
--- a/tests/test_fprint_duplicate.out
+++ b/tests/gnu/fprint_duplicate.out
diff --git a/tests/gnu/fprint_duplicate.sh b/tests/gnu/fprint_duplicate.sh
new file mode 100644
index 0000000..8533b05
--- /dev/null
+++ b/tests/gnu/fprint_duplicate.sh
@@ -0,0 +1,7 @@
+"$XTOUCH" -p "$TEST/foo.out"
+ln "$TEST/foo.out" "$TEST/foo.hard"
+ln -s foo.out "$TEST/foo.soft"
+
+invoke_bfs basic -fprint "$TEST/foo.out" -fprint "$TEST/foo.hard" -fprint "$TEST/foo.soft"
+sort "$TEST/foo.out" >"$OUT"
+diff_output
diff --git a/tests/gnu/fprint_error.sh b/tests/gnu/fprint_error.sh
new file mode 100644
index 0000000..7617034
--- /dev/null
+++ b/tests/gnu/fprint_error.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 -fprint /dev/full
diff --git a/tests/gnu/fprint_noarg.sh b/tests/gnu/fprint_noarg.sh
new file mode 100644
index 0000000..8511649
--- /dev/null
+++ b/tests/gnu/fprint_noarg.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprint
diff --git a/tests/gnu/fprint_noerror.sh b/tests/gnu/fprint_noerror.sh
new file mode 100644
index 0000000..f13a62b
--- /dev/null
+++ b/tests/gnu/fprint_noerror.sh
@@ -0,0 +1,3 @@
+# Regression test: /dev/full should not fail until actually written to
+test -e /dev/full || skip
+invoke_bfs basic -false -fprint /dev/full
diff --git a/tests/gnu/fprint_nonexistent.sh b/tests/gnu/fprint_nonexistent.sh
new file mode 100644
index 0000000..2a403a2
--- /dev/null
+++ b/tests/gnu/fprint_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprint nonexistent/path
diff --git a/tests/test_printf_leak.out b/tests/gnu/fprint_truncate.out
index 15a13db..15a13db 100644
--- a/tests/test_printf_leak.out
+++ b/tests/gnu/fprint_truncate.out
diff --git a/tests/gnu/fprint_truncate.sh b/tests/gnu/fprint_truncate.sh
new file mode 100644
index 0000000..db58a7a
--- /dev/null
+++ b/tests/gnu/fprint_truncate.sh
@@ -0,0 +1,5 @@
+printf "basic\nbasic\n" >"$OUT"
+
+invoke_bfs basic -maxdepth 0 -fprint "$OUT"
+sort_output
+diff_output
diff --git a/tests/test_fprintf.out b/tests/gnu/fprintf.out
index 77ce17a..77ce17a 100644
--- a/tests/test_fprintf.out
+++ b/tests/gnu/fprintf.out
diff --git a/tests/gnu/fprintf.sh b/tests/gnu/fprintf.sh
new file mode 100644
index 0000000..9c6355a
--- /dev/null
+++ b/tests/gnu/fprintf.sh
@@ -0,0 +1,3 @@
+invoke_bfs basic -fprintf "$OUT" '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
+sort_output
+diff_output
diff --git a/tests/gnu/fprintf_nofile.sh b/tests/gnu/fprintf_nofile.sh
new file mode 100644
index 0000000..4e79002
--- /dev/null
+++ b/tests/gnu/fprintf_nofile.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf
diff --git a/tests/gnu/fprintf_noformat.sh b/tests/gnu/fprintf_noformat.sh
new file mode 100644
index 0000000..fd97f4c
--- /dev/null
+++ b/tests/gnu/fprintf_noformat.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf /dev/null
diff --git a/tests/gnu/fprintf_nonexistent.sh b/tests/gnu/fprintf_nonexistent.sh
new file mode 100644
index 0000000..b1eea10
--- /dev/null
+++ b/tests/gnu/fprintf_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -fprintf nonexistent/path '%p\n'
diff --git a/tests/gnu/fstype.out b/tests/gnu/fstype.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/fstype.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/fstype.sh b/tests/gnu/fstype.sh
new file mode 100644
index 0000000..05645c3
--- /dev/null
+++ b/tests/gnu/fstype.sh
@@ -0,0 +1,2 @@
+fstype=$(invoke_bfs basic -maxdepth 0 -printf '%F\n') || skip
+bfs_diff basic -fstype "$fstype"
diff --git a/tests/gnu/fstype_stacked.out b/tests/gnu/fstype_stacked.out
new file mode 100644
index 0000000..c1e0e6c
--- /dev/null
+++ b/tests/gnu/fstype_stacked.out
@@ -0,0 +1 @@
+mnt
diff --git a/tests/gnu/fstype_stacked.sh b/tests/gnu/fstype_stacked.sh
new file mode 100644
index 0000000..a9739bb
--- /dev/null
+++ b/tests/gnu/fstype_stacked.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkdir mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_sudo mount -t ramfs ramfs mnt || skip
+defer bfs_sudo umount mnt
+
+bfs_diff mnt -fstype ramfs -print -o -printf '%p: %F\n'
diff --git a/tests/test_perm_leading_plus_symbolic_minus.out b/tests/gnu/fstype_umount.out
index e69de29..e69de29 100644
--- a/tests/test_perm_leading_plus_symbolic_minus.out
+++ b/tests/gnu/fstype_umount.out
diff --git a/tests/gnu/fstype_umount.sh b/tests/gnu/fstype_umount.sh
new file mode 100644
index 0000000..81c195f
--- /dev/null
+++ b/tests/gnu/fstype_umount.sh
@@ -0,0 +1,12 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+
+mkdir tmp
+bfs_sudo mount -t tmpfs tmpfs tmp || skip
+defer bfs_sudo umount -R tmp
+
+mkdir tmp/ram
+bfs_sudo mount -t ramfs ramfs tmp/ram || skip
+
+bfs_diff tmp -path tmp -exec "${SUDO[@]}" umount tmp/ram \; , -fstype ramfs -print
diff --git a/tests/gnu/gid.out b/tests/gnu/gid.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/gid.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/gid.sh b/tests/gnu/gid.sh
new file mode 100644
index 0000000..2707b4a
--- /dev/null
+++ b/tests/gnu/gid.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "$(id -g)"
diff --git a/tests/gnu/gid_minus.out b/tests/gnu/gid_minus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/gid_minus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/gid_minus.sh b/tests/gnu/gid_minus.sh
new file mode 100644
index 0000000..e3822f0
--- /dev/null
+++ b/tests/gnu/gid_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "-$(($(id -g) + 1))"
diff --git a/tests/gnu/gid_minus_plus.out b/tests/gnu/gid_minus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/gid_minus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/gid_minus_plus.sh b/tests/gnu/gid_minus_plus.sh
new file mode 100644
index 0000000..4ff0877
--- /dev/null
+++ b/tests/gnu/gid_minus_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -gid "-+$(($(id -g) + 1))"
diff --git a/tests/gnu/gid_plus.out b/tests/gnu/gid_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/gid_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/gid_plus.sh b/tests/gnu/gid_plus.sh
new file mode 100644
index 0000000..ccba0e6
--- /dev/null
+++ b/tests/gnu/gid_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -g)" -eq 0 && skip
+bfs_diff basic -gid +0
diff --git a/tests/gnu/gid_plus_plus.out b/tests/gnu/gid_plus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/gid_plus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/gid_plus_plus.sh b/tests/gnu/gid_plus_plus.sh
new file mode 100644
index 0000000..ec7ae86
--- /dev/null
+++ b/tests/gnu/gid_plus_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -g)" -eq 0 && skip
+bfs_diff basic -gid ++0
diff --git a/tests/gnu/ignore_readdir_race.sh b/tests/gnu/ignore_readdir_race.sh
new file mode 100644
index 0000000..75165f6
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race.sh
@@ -0,0 +1,5 @@
+cd "$TEST"
+"$XTOUCH" foo bar
+
+# -links 1 forces a stat() call, which will fail for the second file
+invoke_bfs . -mindepth 1 -ignore_readdir_race -links 1 -exec "$TESTS/remove-sibling.sh" {} \;
diff --git a/tests/gnu/ignore_readdir_race_notdir.sh b/tests/gnu/ignore_readdir_race_notdir.sh
new file mode 100644
index 0000000..12e9fe6
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_notdir.sh
@@ -0,0 +1,7 @@
+# Check -ignore_readdir_race handling when a directory is replaced with a file
+cd "$TEST"
+mkdir foo
+
+invoke_bfs . -mindepth 1 -ignore_readdir_race \
+ -type d -execdir rmdir {} \; \
+ -execdir "$XTOUCH" {} \;
diff --git a/tests/gnu/ignore_readdir_race_root.sh b/tests/gnu/ignore_readdir_race_root.sh
new file mode 100644
index 0000000..dc41e7f
--- /dev/null
+++ b/tests/gnu/ignore_readdir_race_root.sh
@@ -0,0 +1,2 @@
+# Make sure -ignore_readdir_race doesn't suppress ENOENT at the root
+! invoke_bfs basic/nonexistent -ignore_readdir_race
diff --git a/tests/gnu/inum_automount.out b/tests/gnu/inum_automount.out
new file mode 100644
index 0000000..3378e2d
--- /dev/null
+++ b/tests/gnu/inum_automount.out
@@ -0,0 +1 @@
+./automnt
diff --git a/tests/gnu/inum_automount.sh b/tests/gnu/inum_automount.sh
new file mode 100644
index 0000000..86b23e1
--- /dev/null
+++ b/tests/gnu/inum_automount.sh
@@ -0,0 +1,14 @@
+# bfs shouldn't trigger automounts unless it descends into them
+
+command -v systemd-mount &>/dev/null || skip
+
+cd "$TEST"
+mkdir foo automnt
+
+bfs_sudo systemd-mount -A -o bind "$TMP/basic" automnt || skip
+defer bfs_sudo systemd-umount automnt
+
+before=$(inum automnt)
+bfs_diff . -inum "$before" -prune
+after=$(inum automnt)
+((before == after))
diff --git a/tests/test_ipath.out b/tests/gnu/iwholename.out
index 0d36df9..ae1ae21 100644
--- a/tests/test_ipath.out
+++ b/tests/gnu/iwholename.out
@@ -1,7 +1,7 @@
basic/e/f
basic/j/foo
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/gnu/iwholename.sh b/tests/gnu/iwholename.sh
new file mode 100644
index 0000000..0b2d038
--- /dev/null
+++ b/tests/gnu/iwholename.sh
@@ -0,0 +1,2 @@
+invoke_bfs -quit -iwholename PATTERN || skip
+bfs_diff basic -iwholename 'basic/*F*'
diff --git a/tests/test_newer_link.out b/tests/gnu/newer_link.out
index d2dcdd1..d2dcdd1 100644
--- a/tests/test_newer_link.out
+++ b/tests/gnu/newer_link.out
diff --git a/tests/gnu/newer_link.sh b/tests/gnu/newer_link.sh
new file mode 100644
index 0000000..685ac78
--- /dev/null
+++ b/tests/gnu/newer_link.sh
@@ -0,0 +1 @@
+bfs_diff times -newer times/l
diff --git a/tests/gnu/noleaf.out b/tests/gnu/noleaf.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/noleaf.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/noleaf.sh b/tests/gnu/noleaf.sh
new file mode 100644
index 0000000..b19438c
--- /dev/null
+++ b/tests/gnu/noleaf.sh
@@ -0,0 +1 @@
+bfs_diff basic -noleaf
diff --git a/tests/test_bang.out b/tests/gnu/not.out
index 2501b2f..b286454 100644
--- a/tests/test_bang.out
+++ b/tests/gnu/not.out
@@ -2,15 +2,15 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/k/foo/bar
+basic/l
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/gnu/not.sh b/tests/gnu/not.sh
new file mode 100644
index 0000000..9fa9edc
--- /dev/null
+++ b/tests/gnu/not.sh
@@ -0,0 +1 @@
+bfs_diff basic -not -name foo
diff --git a/tests/test_quit_after_print.out b/tests/gnu/not_reachability.out
index 15a13db..15a13db 100644
--- a/tests/test_quit_after_print.out
+++ b/tests/gnu/not_reachability.out
diff --git a/tests/gnu/not_reachability.sh b/tests/gnu/not_reachability.sh
new file mode 100644
index 0000000..7fd3c74
--- /dev/null
+++ b/tests/gnu/not_reachability.sh
@@ -0,0 +1 @@
+bfs_diff basic -print \! -quit -print
diff --git a/tests/gnu/ok_flush.out b/tests/gnu/ok_flush.out
new file mode 100644
index 0000000..6731408
--- /dev/null
+++ b/tests/gnu/ok_flush.out
@@ -0,0 +1,19 @@
+basic ? found
+basic/a ? found
+basic/b ? found
+basic/c ? found
+basic/c/d ? found
+basic/e ? found
+basic/e/f ? found
+basic/g ? found
+basic/g/h ? found
+basic/i ? found
+basic/j ? found
+basic/j/foo ? found
+basic/k ? found
+basic/k/foo ? found
+basic/k/foo/bar ? found
+basic/l ? found
+basic/l/foo ? found
+basic/l/foo/bar ? found
+basic/l/foo/bar/baz ? found
diff --git a/tests/gnu/ok_flush.sh b/tests/gnu/ok_flush.sh
new file mode 100644
index 0000000..87c7298
--- /dev/null
+++ b/tests/gnu/ok_flush.sh
@@ -0,0 +1,4 @@
+# I/O streams should be flushed before -ok prompts
+yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | tr '\0' ' ' | sed 's/?.*?/?/' >"$OUT"
+sort_output
+diff_output
diff --git a/tests/gnu/ok_nothing.sh b/tests/gnu/ok_nothing.sh
new file mode 100644
index 0000000..52c3547
--- /dev/null
+++ b/tests/gnu/ok_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't segfault on missing command
+! invoke_bfs basic -ok \;
diff --git a/tests/gnu/okdir_path_dot.sh b/tests/gnu/okdir_path_dot.sh
new file mode 100644
index 0000000..5b40e27
--- /dev/null
+++ b/tests/gnu/okdir_path_dot.sh
@@ -0,0 +1 @@
+! PATH=".:$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/gnu/okdir_path_empty.sh b/tests/gnu/okdir_path_empty.sh
new file mode 100644
index 0000000..2669ee8
--- /dev/null
+++ b/tests/gnu/okdir_path_empty.sh
@@ -0,0 +1 @@
+! PATH=":$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/gnu/okdir_path_relative.sh b/tests/gnu/okdir_path_relative.sh
new file mode 100644
index 0000000..05100a1
--- /dev/null
+++ b/tests/gnu/okdir_path_relative.sh
@@ -0,0 +1 @@
+! PATH="foo:$PATH" invoke_bfs basic -okdir echo {} \;
diff --git a/tests/test_o.out b/tests/gnu/or.out
index 9a82ee2..1650c4d 100644
--- a/tests/test_o.out
+++ b/tests/gnu/or.out
@@ -2,12 +2,12 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/gnu/or.sh b/tests/gnu/or.sh
new file mode 100644
index 0000000..eb28030
--- /dev/null
+++ b/tests/gnu/or.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -or -type d
diff --git a/tests/gnu/path_d.out b/tests/gnu/path_d.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/path_d.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/path_d.sh b/tests/gnu/path_d.sh
new file mode 100644
index 0000000..e546ff3
--- /dev/null
+++ b/tests/gnu/path_d.sh
@@ -0,0 +1 @@
+bfs_diff basic -d
diff --git a/tests/test_perm_000_plus.out b/tests/gnu/perm_000_slash.out
index d7494b8..d7494b8 100644
--- a/tests/test_perm_000_plus.out
+++ b/tests/gnu/perm_000_slash.out
diff --git a/tests/gnu/perm_000_slash.sh b/tests/gnu/perm_000_slash.sh
new file mode 100644
index 0000000..f4b2665
--- /dev/null
+++ b/tests/gnu/perm_000_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /000
diff --git a/tests/test_perm_222_slash.out b/tests/gnu/perm_222_slash.out
index 9a5b95a..9a5b95a 100644
--- a/tests/test_perm_222_slash.out
+++ b/tests/gnu/perm_222_slash.out
diff --git a/tests/gnu/perm_222_slash.sh b/tests/gnu/perm_222_slash.sh
new file mode 100644
index 0000000..f4be665
--- /dev/null
+++ b/tests/gnu/perm_222_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /222
diff --git a/tests/test_perm_644_slash.out b/tests/gnu/perm_644_slash.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_644_slash.out
+++ b/tests/gnu/perm_644_slash.out
diff --git a/tests/gnu/perm_644_slash.sh b/tests/gnu/perm_644_slash.sh
new file mode 100644
index 0000000..e883f17
--- /dev/null
+++ b/tests/gnu/perm_644_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /644
diff --git a/tests/test_perm_leading_plus_symbolic_slash.out b/tests/gnu/perm_leading_plus_symbolic_slash.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_leading_plus_symbolic_slash.out
+++ b/tests/gnu/perm_leading_plus_symbolic_slash.out
diff --git a/tests/gnu/perm_leading_plus_symbolic_slash.sh b/tests/gnu/perm_leading_plus_symbolic_slash.sh
new file mode 100644
index 0000000..3db27bd
--- /dev/null
+++ b/tests/gnu/perm_leading_plus_symbolic_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /+rwx
diff --git a/tests/test_perm_symbolic_slash.out b/tests/gnu/perm_symbolic_slash.out
index 7e5ae98..7e5ae98 100644
--- a/tests/test_perm_symbolic_slash.out
+++ b/tests/gnu/perm_symbolic_slash.out
diff --git a/tests/gnu/perm_symbolic_slash.sh b/tests/gnu/perm_symbolic_slash.sh
new file mode 100644
index 0000000..253b14e
--- /dev/null
+++ b/tests/gnu/perm_symbolic_slash.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm /a+r,u=wX,g+wX-w
diff --git a/tests/test_precedence.out b/tests/gnu/precedence.out
index b3d7a51..7f589f2 100644
--- a/tests/test_precedence.out
+++ b/tests/gnu/precedence.out
@@ -1,4 +1,4 @@
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l/foo
basic/l/foo/bar/baz
diff --git a/tests/gnu/precedence.sh b/tests/gnu/precedence.sh
new file mode 100644
index 0000000..b35d160
--- /dev/null
+++ b/tests/gnu/precedence.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -name foo -type d -o -name bar -a -type f \) -print , \! -empty -type f -print
diff --git a/tests/test_print0.out b/tests/gnu/print0.out
index 1347444..1347444 100644
--- a/tests/test_print0.out
+++ b/tests/gnu/print0.out
Binary files differ
diff --git a/tests/gnu/print0.sh b/tests/gnu/print0.sh
new file mode 100644
index 0000000..b916172
--- /dev/null
+++ b/tests/gnu/print0.sh
@@ -0,0 +1,2 @@
+invoke_bfs basic/a basic/b -print0 >"$OUT"
+diff_output
diff --git a/tests/gnu/print_error.sh b/tests/gnu/print_error.sh
new file mode 100644
index 0000000..bc79637
--- /dev/null
+++ b/tests/gnu/print_error.sh
@@ -0,0 +1,2 @@
+test -e /dev/full || skip
+! invoke_bfs basic -maxdepth 0 >/dev/full
diff --git a/tests/test_printf.out b/tests/gnu/printf.out
index 801ddbb..77ce17a 100644
--- a/tests/test_printf.out
+++ b/tests/gnu/printf.out
@@ -2,18 +2,18 @@
%p(basic/a) %d(1) %f(a) %h(basic) %H(basic) %P(a) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/b) %d(1) %f(b) %h(basic) %H(basic) %P(b) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/c) %d(1) %f(c) %h(basic) %H(basic) %P(c) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/e) %d(1) %f(e) %h(basic) %H(basic) %P(e) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %m(644) %M(-rw-r--r--) %y(f)
%p(basic/g) %d(1) %f(g) %h(basic) %H(basic) %P(g) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/i) %d(1) %f(i) %h(basic) %H(basic) %P(i) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/j) %d(1) %f(j) %h(basic) %H(basic) %P(j) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/k) %d(1) %f(k) %h(basic) %H(basic) %P(k) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/l) %d(1) %f(l) %h(basic) %H(basic) %P(l) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %m(644) %M(-rw-r--r--) %y(f)
-%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %m(644) %M(-rw-r--r--) %y(f)
-%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/j/foo) %d(2) %f(foo) %h(basic/j) %H(basic) %P(j/foo) %m(644) %M(-rw-r--r--) %y(f)
+%p(basic/k) %d(1) %f(k) %h(basic) %H(basic) %P(k) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/k/foo) %d(2) %f(foo) %h(basic/k) %H(basic) %P(k/foo) %m(755) %M(drwxr-xr-x) %y(d)
-%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/k/foo/bar) %d(3) %f(bar) %h(basic/k/foo) %H(basic) %P(k/foo/bar) %m(644) %M(-rw-r--r--) %y(f)
+%p(basic/l) %d(1) %f(l) %h(basic) %H(basic) %P(l) %m(755) %M(drwxr-xr-x) %y(d)
+%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/l/foo/bar) %d(3) %f(bar) %h(basic/l/foo) %H(basic) %P(l/foo/bar) %m(755) %M(drwxr-xr-x) %y(d)
%p(basic/l/foo/bar/baz) %d(4) %f(baz) %h(basic/l/foo/bar) %H(basic) %P(l/foo/bar/baz) %m(644) %M(-rw-r--r--) %y(f)
diff --git a/tests/gnu/printf.sh b/tests/gnu/printf.sh
new file mode 100644
index 0000000..4dd48e8
--- /dev/null
+++ b/tests/gnu/printf.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%m(%m) %%M(%M) %%y(%y)\n'
diff --git a/tests/test_printf_H.out b/tests/gnu/printf_H.out
index 116b425..6b605ff 100644
--- a/tests/test_printf_H.out
+++ b/tests/gnu/printf_H.out
@@ -1,32 +1,32 @@
%p(basic) %d(0) %f(basic) %h(.) %H(basic) %P() %y(d)
-%p(links) %d(0) %f(links) %h(.) %H(links) %P() %y(d)
%p(basic/a) %d(1) %f(a) %h(basic) %H(basic) %P(a) %y(f)
%p(basic/b) %d(1) %f(b) %h(basic) %H(basic) %P(b) %y(f)
%p(basic/c) %d(1) %f(c) %h(basic) %H(basic) %P(c) %y(d)
+%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %y(f)
%p(basic/e) %d(1) %f(e) %h(basic) %H(basic) %P(e) %y(d)
+%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %y(f)
%p(basic/g) %d(1) %f(g) %h(basic) %H(basic) %P(g) %y(d)
+%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %y(d)
%p(basic/i) %d(1) %f(i) %h(basic) %H(basic) %P(i) %y(d)
%p(basic/j) %d(1) %f(j) %h(basic) %H(basic) %P(j) %y(d)
+%p(basic/j/foo) %d(2) %f(foo) %h(basic/j) %H(basic) %P(j/foo) %y(f)
%p(basic/k) %d(1) %f(k) %h(basic) %H(basic) %P(k) %y(d)
+%p(basic/k/foo) %d(2) %f(foo) %h(basic/k) %H(basic) %P(k/foo) %y(d)
+%p(basic/k/foo/bar) %d(3) %f(bar) %h(basic/k/foo) %H(basic) %P(k/foo/bar) %y(f)
%p(basic/l) %d(1) %f(l) %h(basic) %H(basic) %P(l) %y(d)
+%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %y(d)
+%p(basic/l/foo/bar) %d(3) %f(bar) %h(basic/l/foo) %H(basic) %P(l/foo/bar) %y(d)
+%p(basic/l/foo/bar/baz) %d(4) %f(baz) %h(basic/l/foo/bar) %H(basic) %P(l/foo/bar/baz) %y(f)
+%p(links) %d(0) %f(links) %h(.) %H(links) %P() %y(d)
%p(links/broken) %d(1) %f(broken) %h(links) %H(links) %P(broken) %y(l)
%p(links/deeply) %d(1) %f(deeply) %h(links) %H(links) %P(deeply) %y(d)
-%p(links/file) %d(1) %f(file) %h(links) %H(links) %P(file) %y(f)
-%p(links/hardlink) %d(1) %f(hardlink) %h(links) %H(links) %P(hardlink) %y(f)
-%p(links/notdir) %d(1) %f(notdir) %h(links) %H(links) %P(notdir) %y(l)
-%p(links/skip) %d(1) %f(skip) %h(links) %H(links) %P(skip) %y(l)
-%p(links/symlink) %d(1) %f(symlink) %h(links) %H(links) %P(symlink) %y(l)
-%p(basic/c/d) %d(2) %f(d) %h(basic/c) %H(basic) %P(c/d) %y(f)
-%p(basic/e/f) %d(2) %f(f) %h(basic/e) %H(basic) %P(e/f) %y(f)
-%p(basic/g/h) %d(2) %f(h) %h(basic/g) %H(basic) %P(g/h) %y(d)
-%p(basic/j/foo) %d(2) %f(foo) %h(basic/j) %H(basic) %P(j/foo) %y(f)
-%p(basic/k/foo) %d(2) %f(foo) %h(basic/k) %H(basic) %P(k/foo) %y(d)
-%p(basic/l/foo) %d(2) %f(foo) %h(basic/l) %H(basic) %P(l/foo) %y(d)
%p(links/deeply/nested) %d(2) %f(nested) %h(links/deeply) %H(links) %P(deeply/nested) %y(d)
-%p(basic/k/foo/bar) %d(3) %f(bar) %h(basic/k/foo) %H(basic) %P(k/foo/bar) %y(f)
-%p(basic/l/foo/bar) %d(3) %f(bar) %h(basic/l/foo) %H(basic) %P(l/foo/bar) %y(d)
%p(links/deeply/nested/broken) %d(3) %f(broken) %h(links/deeply/nested) %H(links) %P(deeply/nested/broken) %y(l)
%p(links/deeply/nested/dir) %d(3) %f(dir) %h(links/deeply/nested) %H(links) %P(deeply/nested/dir) %y(d)
%p(links/deeply/nested/file) %d(3) %f(file) %h(links/deeply/nested) %H(links) %P(deeply/nested/file) %y(f)
%p(links/deeply/nested/link) %d(3) %f(link) %h(links/deeply/nested) %H(links) %P(deeply/nested/link) %y(l)
-%p(basic/l/foo/bar/baz) %d(4) %f(baz) %h(basic/l/foo/bar) %H(basic) %P(l/foo/bar/baz) %y(f)
+%p(links/file) %d(1) %f(file) %h(links) %H(links) %P(file) %y(f)
+%p(links/hardlink) %d(1) %f(hardlink) %h(links) %H(links) %P(hardlink) %y(f)
+%p(links/notdir) %d(1) %f(notdir) %h(links) %H(links) %P(notdir) %y(l)
+%p(links/skip) %d(1) %f(skip) %h(links) %H(links) %P(skip) %y(l)
+%p(links/symlink) %d(1) %f(symlink) %h(links) %H(links) %P(symlink) %y(l)
diff --git a/tests/gnu/printf_H.sh b/tests/gnu/printf_H.sh
new file mode 100644
index 0000000..ddef7e2
--- /dev/null
+++ b/tests/gnu/printf_H.sh
@@ -0,0 +1 @@
+bfs_diff basic links -printf '%%p(%p) %%d(%d) %%f(%f) %%h(%h) %%H(%H) %%P(%P) %%y(%y)\n'
diff --git a/tests/gnu/printf_Y_error.out b/tests/gnu/printf_Y_error.out
new file mode 100644
index 0000000..1dd554e
--- /dev/null
+++ b/tests/gnu/printf_Y_error.out
@@ -0,0 +1,3 @@
+(.) () d d
+(./bar) (foo/bar) l ?
+(./foo) () d d
diff --git a/tests/gnu/printf_Y_error.sh b/tests/gnu/printf_Y_error.sh
new file mode 100644
index 0000000..d3130ce
--- /dev/null
+++ b/tests/gnu/printf_Y_error.sh
@@ -0,0 +1,8 @@
+cd "$TEST"
+mkdir foo
+ln -s foo/bar bar
+
+chmod -x foo
+defer chmod +x foo
+
+! bfs_diff . -printf '(%p) (%l) %y %Y\n'
diff --git a/tests/test_perm_symbolic.out b/tests/gnu/printf_empty.out
index e69de29..e69de29 100644
--- a/tests/test_perm_symbolic.out
+++ b/tests/gnu/printf_empty.out
diff --git a/tests/gnu/printf_empty.sh b/tests/gnu/printf_empty.sh
new file mode 100644
index 0000000..ed5eb04
--- /dev/null
+++ b/tests/gnu/printf_empty.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf ''
diff --git a/tests/test_printf_escapes.out b/tests/gnu/printf_escapes.out
index 20ea120..20ea120 100644
--- a/tests/test_printf_escapes.out
+++ b/tests/gnu/printf_escapes.out
diff --git a/tests/gnu/printf_escapes.sh b/tests/gnu/printf_escapes.sh
new file mode 100644
index 0000000..ece7c0e
--- /dev/null
+++ b/tests/gnu/printf_escapes.sh
@@ -0,0 +1 @@
+bfs_diff basic -maxdepth 0 -printf '\18\118\1118\11118\n\cfoo'
diff --git a/tests/test_printf_flags.out b/tests/gnu/printf_flags.out
index 1a92b6e..c2c1f0a 100644
--- a/tests/test_printf_flags.out
+++ b/tests/gnu/printf_flags.out
@@ -2,18 +2,18 @@
|basic/a | +01 0644
|basic/b | +01 0644
|basic/c | +01 0755
+|basic/c/d | +02 0644
|basic/e | +01 0755
+|basic/e/f | +02 0644
|basic/g | +01 0755
+|basic/g/h | +02 0755
|basic/i | +01 0755
|basic/j | +01 0755
-|basic/k | +01 0755
-|basic/l | +01 0755
-|basic/c/d | +02 0644
-|basic/e/f | +02 0644
-|basic/g/h | +02 0755
|basic/j/fo| +02 0644
+|basic/k | +01 0755
|basic/k/fo| +02 0755
|basic/k/fo| +03 0644
+|basic/l | +01 0755
|basic/l/fo| +02 0755
|basic/l/fo| +03 0755
|basic/l/fo| +04 0644
diff --git a/tests/gnu/printf_flags.sh b/tests/gnu/printf_flags.sh
new file mode 100644
index 0000000..2ef37ad
--- /dev/null
+++ b/tests/gnu/printf_flags.sh
@@ -0,0 +1 @@
+bfs_diff basic -printf '|%- 10.10p| %+03d %#4m\n'
diff --git a/tests/test_printf_l_nonlink.out b/tests/gnu/printf_l_nonlink.out
index 66e2908..30df155 100644
--- a/tests/test_printf_l_nonlink.out
+++ b/tests/gnu/printf_l_nonlink.out
@@ -1,11 +1,11 @@
| links -> |
| links/file -> |
+| links/skip -> deeply/nested |
| links/broken -> nowhere |
| links/deeply -> |
+| links/notdir -> symlink/file |
| links/symlink -> file |
| links/hardlink -> |
-| links/skip -> deeply/nested |
-| links/notdir -> symlink/file |
| links/deeply/nested -> |
| links/deeply/nested/dir -> |
| links/deeply/nested/file -> |
diff --git a/tests/gnu/printf_l_nonlink.sh b/tests/gnu/printf_l_nonlink.sh
new file mode 100644
index 0000000..1c66442
--- /dev/null
+++ b/tests/gnu/printf_l_nonlink.sh
@@ -0,0 +1 @@
+bfs_diff links -printf '| %26p -> %-26l |\n'
diff --git a/tests/test_quit_implicit_print.out b/tests/gnu/printf_leak.out
index 15a13db..15a13db 100644
--- a/tests/test_quit_implicit_print.out
+++ b/tests/gnu/printf_leak.out
diff --git a/tests/gnu/printf_leak.sh b/tests/gnu/printf_leak.sh
new file mode 100644
index 0000000..c4092c7
--- /dev/null
+++ b/tests/gnu/printf_leak.sh
@@ -0,0 +1,2 @@
+# Memory leak regression test
+bfs_diff basic -maxdepth 0 -printf '%p'
diff --git a/tests/gnu/printf_nul.out b/tests/gnu/printf_nul.out
new file mode 100644
index 0000000..fdb6c6b
--- /dev/null
+++ b/tests/gnu/printf_nul.out
Binary files differ
diff --git a/tests/gnu/printf_nul.sh b/tests/gnu/printf_nul.sh
new file mode 100644
index 0000000..0b8b928
--- /dev/null
+++ b/tests/gnu/printf_nul.sh
@@ -0,0 +1,3 @@
+# NUL byte regression test
+invoke_bfs basic/a basic/b -maxdepth 0 -printf '%h\0%f\n' >"$OUT"
+diff_output
diff --git a/tests/test_printf_slash.out b/tests/gnu/printf_slash.out
index 5571971..5571971 100644
--- a/tests/test_printf_slash.out
+++ b/tests/gnu/printf_slash.out
diff --git a/tests/gnu/printf_slash.sh b/tests/gnu/printf_slash.sh
new file mode 100644
index 0000000..b64ff10
--- /dev/null
+++ b/tests/gnu/printf_slash.sh
@@ -0,0 +1 @@
+bfs_diff / -maxdepth 0 -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_slashes.out b/tests/gnu/printf_slashes.out
index 5571971..5571971 100644
--- a/tests/test_printf_slashes.out
+++ b/tests/gnu/printf_slashes.out
diff --git a/tests/gnu/printf_slashes.sh b/tests/gnu/printf_slashes.sh
new file mode 100644
index 0000000..d56a287
--- /dev/null
+++ b/tests/gnu/printf_slashes.sh
@@ -0,0 +1 @@
+bfs_diff /// -maxdepth 0 -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_times.out b/tests/gnu/printf_times.out
index 7e7d256..7e7d256 100644
--- a/tests/test_printf_times.out
+++ b/tests/gnu/printf_times.out
diff --git a/tests/gnu/printf_times.sh b/tests/gnu/printf_times.sh
new file mode 100644
index 0000000..e4f5155
--- /dev/null
+++ b/tests/gnu/printf_times.sh
@@ -0,0 +1 @@
+bfs_diff times -type f -printf '%p | %a %AY-%Am-%Ad %AH:%AI:%AS %A@ | %t %TY-%Tm-%Td %TH:%TI:%TS %T@\n'
diff --git a/tests/test_printf_trailing_slash.out b/tests/gnu/printf_trailing_slash.out
index 0aa4ffc..017ac0d 100644
--- a/tests/test_printf_trailing_slash.out
+++ b/tests/gnu/printf_trailing_slash.out
@@ -1,3 +1,4 @@
+(.)/(basic/)
(basic)/(a)
(basic)/(b)
(basic)/(c)
@@ -7,13 +8,12 @@
(basic)/(j)
(basic)/(k)
(basic)/(l)
-(.)/(basic/)
(basic/c)/(d)
(basic/e)/(f)
(basic/g)/(h)
(basic/j)/(foo)
(basic/k)/(foo)
-(basic/l)/(foo)
(basic/k/foo)/(bar)
+(basic/l)/(foo)
(basic/l/foo)/(bar)
(basic/l/foo/bar)/(baz)
diff --git a/tests/gnu/printf_trailing_slash.sh b/tests/gnu/printf_trailing_slash.sh
new file mode 100644
index 0000000..2df818d
--- /dev/null
+++ b/tests/gnu/printf_trailing_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/ -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_trailing_slashes.out b/tests/gnu/printf_trailing_slashes.out
index cbb54a8..fd27101 100644
--- a/tests/test_printf_trailing_slashes.out
+++ b/tests/gnu/printf_trailing_slashes.out
@@ -1,3 +1,4 @@
+(.)/(basic///)
(basic//)/(a)
(basic//)/(b)
(basic//)/(c)
@@ -7,13 +8,12 @@
(basic//)/(j)
(basic//)/(k)
(basic//)/(l)
-(.)/(basic///)
(basic///c)/(d)
(basic///e)/(f)
(basic///g)/(h)
(basic///j)/(foo)
(basic///k)/(foo)
-(basic///l)/(foo)
(basic///k/foo)/(bar)
+(basic///l)/(foo)
(basic///l/foo)/(bar)
(basic///l/foo/bar)/(baz)
diff --git a/tests/gnu/printf_trailing_slashes.sh b/tests/gnu/printf_trailing_slashes.sh
new file mode 100644
index 0000000..6dc532c
--- /dev/null
+++ b/tests/gnu/printf_trailing_slashes.sh
@@ -0,0 +1 @@
+bfs_diff basic/// -printf '(%h)/(%f)\n'
diff --git a/tests/test_printf_types.out b/tests/gnu/printf_types.out
index 9cfe347..8144c7c 100644
--- a/tests/test_printf_types.out
+++ b/tests/gnu/printf_types.out
@@ -1,11 +1,11 @@
(loops) () d d
(loops/broken) (nowhere) l N
(loops/deeply) () d d
+(loops/deeply/nested) () d d
+(loops/deeply/nested/dir) () d d
+(loops/deeply/nested/loop) (../../deeply) l d
(loops/file) () f f
(loops/loop) (loop) l L
-(loops/symlink) (file) l f
-(loops/deeply/nested) () d d
(loops/notdir) (symlink/file) l N
-(loops/deeply/nested/dir) () d d
(loops/skip) (deeply/nested/loop/nested) l d
-(loops/deeply/nested/loop) (../../deeply) l d
+(loops/symlink) (file) l f
diff --git a/tests/gnu/printf_types.sh b/tests/gnu/printf_types.sh
new file mode 100644
index 0000000..6ed1d75
--- /dev/null
+++ b/tests/gnu/printf_types.sh
@@ -0,0 +1 @@
+bfs_diff loops -printf '(%p) (%l) %y %Y\n'
diff --git a/tests/gnu/printf_u_g_ulimit.sh b/tests/gnu/printf_u_g_ulimit.sh
new file mode 100644
index 0000000..c621b9b
--- /dev/null
+++ b/tests/gnu/printf_u_g_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+[ "$(invoke_bfs deep -printf '%u %g\n' | uniq)" = "$(id -un) $(id -gn)" ]
diff --git a/tests/test_readable.out b/tests/gnu/readable.out
index 386feba..386feba 100644
--- a/tests/test_readable.out
+++ b/tests/gnu/readable.out
diff --git a/tests/gnu/readable.sh b/tests/gnu/readable.sh
new file mode 100644
index 0000000..a496667
--- /dev/null
+++ b/tests/gnu/readable.sh
@@ -0,0 +1 @@
+bfs_diff perms -readable
diff --git a/tests/gnu/regex_error.sh b/tests/gnu/regex_error.sh
new file mode 100644
index 0000000..4af933f
--- /dev/null
+++ b/tests/gnu/regex_error.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -regex '['
diff --git a/tests/gnu/regex_invalid_utf8.out b/tests/gnu/regex_invalid_utf8.out
new file mode 100644
index 0000000..a133b1a
--- /dev/null
+++ b/tests/gnu/regex_invalid_utf8.out
@@ -0,0 +1 @@
+./â„
diff --git a/tests/gnu/regex_invalid_utf8.sh b/tests/gnu/regex_invalid_utf8.sh
new file mode 100644
index 0000000..7006dcd
--- /dev/null
+++ b/tests/gnu/regex_invalid_utf8.sh
@@ -0,0 +1,8 @@
+cd "$TEST"
+
+# Incomplete UTF-8 sequences
+touch $'\xC3' || skip
+touch $'\xE2\x84' || skip
+touch $'\xF0\x9F\x92' || skip
+
+bfs_diff . -regex '\./..'
diff --git a/tests/test_regextype_posix_basic.out b/tests/gnu/regextype_ed.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_posix_basic.out
+++ b/tests/gnu/regextype_ed.out
diff --git a/tests/gnu/regextype_ed.sh b/tests/gnu/regextype_ed.sh
new file mode 100644
index 0000000..0e92db3
--- /dev/null
+++ b/tests/gnu/regextype_ed.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype ed -regex '\./\((\)'
diff --git a/tests/gnu/regextype_emacs.out b/tests/gnu/regextype_emacs.out
new file mode 100644
index 0000000..95942b4
--- /dev/null
+++ b/tests/gnu/regextype_emacs.out
@@ -0,0 +1,6 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/k/foo/bar
+basic/l/foo
+basic/l/foo/bar
diff --git a/tests/gnu/regextype_emacs.sh b/tests/gnu/regextype_emacs.sh
new file mode 100644
index 0000000..3cc388c
--- /dev/null
+++ b/tests/gnu/regextype_emacs.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype emacs -quit || skip
+
+bfs_diff basic -regextype emacs -regex '.*/\(f+o?o?\|bar\)'
diff --git a/tests/test_name.out b/tests/gnu/regextype_grep.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_name.out
+++ b/tests/gnu/regextype_grep.out
diff --git a/tests/gnu/regextype_grep.sh b/tests/gnu/regextype_grep.sh
new file mode 100644
index 0000000..0830667
--- /dev/null
+++ b/tests/gnu/regextype_grep.sh
@@ -0,0 +1,3 @@
+invoke_bfs -regextype grep -quit || skip
+
+bfs_diff basic -regextype grep -regex '.*/f\+o\?o\?'
diff --git a/tests/test_regextype_posix_extended.out b/tests/gnu/regextype_posix_basic.out
index 0f0971e..0f0971e 100644
--- a/tests/test_regextype_posix_extended.out
+++ b/tests/gnu/regextype_posix_basic.out
diff --git a/tests/gnu/regextype_posix_basic.sh b/tests/gnu/regextype_posix_basic.sh
new file mode 100644
index 0000000..fa2254c
--- /dev/null
+++ b/tests/gnu/regextype_posix_basic.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype posix-basic -regex '\./\((\)'
diff --git a/tests/gnu/regextype_posix_extended.out b/tests/gnu/regextype_posix_extended.out
new file mode 100644
index 0000000..0f0971e
--- /dev/null
+++ b/tests/gnu/regextype_posix_extended.out
@@ -0,0 +1 @@
+./(
diff --git a/tests/gnu/regextype_posix_extended.sh b/tests/gnu/regextype_posix_extended.sh
new file mode 100644
index 0000000..f82ed65
--- /dev/null
+++ b/tests/gnu/regextype_posix_extended.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype posix-extended -regex '\./(\()'
diff --git a/tests/gnu/regextype_sed.out b/tests/gnu/regextype_sed.out
new file mode 100644
index 0000000..0f0971e
--- /dev/null
+++ b/tests/gnu/regextype_sed.out
@@ -0,0 +1 @@
+./(
diff --git a/tests/gnu/regextype_sed.sh b/tests/gnu/regextype_sed.sh
new file mode 100644
index 0000000..9ce6f4e
--- /dev/null
+++ b/tests/gnu/regextype_sed.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -regextype sed -regex '\./\((\)'
diff --git a/tests/gnu/true.out b/tests/gnu/true.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/true.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/true.sh b/tests/gnu/true.sh
new file mode 100644
index 0000000..65f3254
--- /dev/null
+++ b/tests/gnu/true.sh
@@ -0,0 +1 @@
+bfs_diff basic -true
diff --git a/tests/gnu/uid.out b/tests/gnu/uid.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/uid.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/uid.sh b/tests/gnu/uid.sh
new file mode 100644
index 0000000..fb3cd93
--- /dev/null
+++ b/tests/gnu/uid.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "$(id -u)"
diff --git a/tests/gnu/uid_minus.out b/tests/gnu/uid_minus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/uid_minus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/uid_minus.sh b/tests/gnu/uid_minus.sh
new file mode 100644
index 0000000..6d371f2
--- /dev/null
+++ b/tests/gnu/uid_minus.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "-$(($(id -u) + 1))"
diff --git a/tests/gnu/uid_minus_plus.out b/tests/gnu/uid_minus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/uid_minus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/uid_minus_plus.sh b/tests/gnu/uid_minus_plus.sh
new file mode 100644
index 0000000..e7a0496
--- /dev/null
+++ b/tests/gnu/uid_minus_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -uid "-+$(($(id -u) + 1))"
diff --git a/tests/gnu/uid_plus.out b/tests/gnu/uid_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/uid_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/uid_plus.sh b/tests/gnu/uid_plus.sh
new file mode 100644
index 0000000..22b2c8e
--- /dev/null
+++ b/tests/gnu/uid_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -u)" -eq 0 && skip
+bfs_diff basic -uid +0
diff --git a/tests/gnu/uid_plus_plus.out b/tests/gnu/uid_plus_plus.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/gnu/uid_plus_plus.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/uid_plus_plus.sh b/tests/gnu/uid_plus_plus.sh
new file mode 100644
index 0000000..e021888
--- /dev/null
+++ b/tests/gnu/uid_plus_plus.sh
@@ -0,0 +1,2 @@
+test "$(id -u)" -eq 0 && skip
+bfs_diff basic -uid ++0
diff --git a/tests/gnu/used.out b/tests/gnu/used.out
new file mode 100644
index 0000000..647621b
--- /dev/null
+++ b/tests/gnu/used.out
@@ -0,0 +1,4 @@
+-used +7: ./nextyear
+-used 1: ./tomorrow
+-used 2: ./dayafter
+-used 7: ./nextweek
diff --git a/tests/gnu/used.sh b/tests/gnu/used.sh
new file mode 100644
index 0000000..5e5d4e9
--- /dev/null
+++ b/tests/gnu/used.sh
@@ -0,0 +1,40 @@
+iso8601() {
+ printf '%04d-%02d-%02dT%02d:%02d:%02d\n' "$@"
+}
+
+cd "$TEST"
+
+now=$(date '+%Y-%m-%dT%H:%M:%S')
+
+# Parse the current date
+[[ "$now" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]] || fail
+# Treat leading zeros as decimal, not octal
+YYYY=$((10#${BASH_REMATCH[1]}))
+MM=$((10#${BASH_REMATCH[2]}))
+DD=$((10#${BASH_REMATCH[3]}))
+hh=$((10#${BASH_REMATCH[4]}))
+mm=$((10#${BASH_REMATCH[5]}))
+ss=$((10#${BASH_REMATCH[6]}))
+
+# -used is always false if atime < ctime
+yesterday=$(iso8601 $YYYY $MM $((DD - 1)) $hh $mm $ss)
+"$XTOUCH" -at "$yesterday" yesterday
+
+# -used rounds up
+tomorrow=$(iso8601 $YYYY $MM $DD $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$tomorrow" tomorrow
+
+dayafter=$(iso8601 $YYYY $MM $((DD + 1)) $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$dayafter" dayafter
+
+nextweek=$(iso8601 $YYYY $MM $((DD + 6)) $((hh + 1)) $mm $ss)
+"$XTOUCH" -at "$nextweek" nextweek
+
+nextyear=$(iso8601 $((YYYY + 1)) $MM $DD $hh $mm $ss)
+"$XTOUCH" -at "$nextyear" nextyear
+
+bfs_diff -mindepth 1 \
+ -a -used 1 -printf '-used 1: %p\n' \
+ -o -used 2 -printf '-used 2: %p\n' \
+ -o -used 7 -printf '-used 7: %p\n' \
+ -o -used +7 -printf '-used +7: %p\n'
diff --git a/tests/gnu/wholename.out b/tests/gnu/wholename.out
new file mode 100644
index 0000000..ae1ae21
--- /dev/null
+++ b/tests/gnu/wholename.out
@@ -0,0 +1,7 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/k/foo/bar
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/gnu/wholename.sh b/tests/gnu/wholename.sh
new file mode 100644
index 0000000..4c641b8
--- /dev/null
+++ b/tests/gnu/wholename.sh
@@ -0,0 +1 @@
+bfs_diff basic -wholename 'basic/*f*'
diff --git a/tests/test_writable.out b/tests/gnu/writable.out
index 9a5b95a..9a5b95a 100644
--- a/tests/test_writable.out
+++ b/tests/gnu/writable.out
diff --git a/tests/gnu/writable.sh b/tests/gnu/writable.sh
new file mode 100644
index 0000000..93c666f
--- /dev/null
+++ b/tests/gnu/writable.sh
@@ -0,0 +1 @@
+bfs_diff perms -writable
diff --git a/tests/gnu/xtype_bind_mount.out b/tests/gnu/xtype_bind_mount.out
new file mode 100644
index 0000000..d18d706
--- /dev/null
+++ b/tests/gnu/xtype_bind_mount.out
@@ -0,0 +1,2 @@
+./link
+./null
diff --git a/tests/gnu/xtype_bind_mount.sh b/tests/gnu/xtype_bind_mount.sh
new file mode 100644
index 0000000..35fb3f5
--- /dev/null
+++ b/tests/gnu/xtype_bind_mount.sh
@@ -0,0 +1,10 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" file null
+ln -s /dev/null link
+
+bfs_sudo mount --bind /dev/null null || skip
+defer bfs_sudo umount null
+
+bfs_diff . -xtype c
diff --git a/tests/test_xtype_f.out b/tests/gnu/xtype_f.out
index dbe0c3f..e6ba322 100644
--- a/tests/test_xtype_f.out
+++ b/tests/gnu/xtype_f.out
@@ -1,5 +1,5 @@
+links/deeply/nested/file
+links/deeply/nested/link
links/file
links/hardlink
links/symlink
-links/deeply/nested/file
-links/deeply/nested/link
diff --git a/tests/gnu/xtype_f.sh b/tests/gnu/xtype_f.sh
new file mode 100644
index 0000000..0ea27bb
--- /dev/null
+++ b/tests/gnu/xtype_f.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype f
diff --git a/tests/test_xtype_l.out b/tests/gnu/xtype_l.out
index 62d3ae4..f29c978 100644
--- a/tests/test_xtype_l.out
+++ b/tests/gnu/xtype_l.out
@@ -1,3 +1,3 @@
links/broken
-links/notdir
links/deeply/nested/broken
+links/notdir
diff --git a/tests/gnu/xtype_l.sh b/tests/gnu/xtype_l.sh
new file mode 100644
index 0000000..39c8ea4
--- /dev/null
+++ b/tests/gnu/xtype_l.sh
@@ -0,0 +1 @@
+bfs_diff links -xtype l
diff --git a/tests/ioq.c b/tests/ioq.c
new file mode 100644
index 0000000..ef5ee3b
--- /dev/null
+++ b/tests/ioq.c
@@ -0,0 +1,77 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "ioq.h"
+#include "bfstd.h"
+#include "diag.h"
+#include "dir.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+
+/**
+ * Test for blocking within ioq_slot_push().
+ *
+ * struct ioqq only supports non-blocking reads; if a write encounters a full
+ * slot, it must block until someone pops from that slot:
+ *
+ * Reader Writer
+ * ────────────────────────── ─────────────────────────
+ * tail: 0 → 1
+ * slots[0]: empty → full
+ * tail: 1 → 0
+ * slots[1]: empty → full
+ * tail: 0 → 1
+ * slots[0]: full → full* (IOQ_BLOCKED)
+ * ioq_slot_wait() ...
+ * head: 0 → 1
+ * slots[0]: full* → empty
+ * ioq_slot_wake()
+ * ...
+ * slots[0]: empty → full
+ *
+ * To reproduce this unlikely scenario, we must fill up the ready queue, then
+ * call ioq_cancel() which pushes an additional sentinel IOQ_STOP operation.
+ */
+static void check_ioq_push_block(void) {
+ // Must be a power of two to fill the entire queue
+ const size_t depth = 2;
+
+ struct ioq *ioq = ioq_create(depth, 1);
+ bfs_verify(ioq, "ioq_create(): %s", xstrerror(errno));
+
+ // Push enough operations to fill the queue
+ for (size_t i = 0; i < depth; ++i) {
+ struct bfs_dir *dir = bfs_allocdir();
+ bfs_verify(dir, "bfs_allocdir(): %s", xstrerror(errno));
+
+ int ret = ioq_opendir(ioq, dir, AT_FDCWD, ".", 0, NULL);
+ bfs_verify(ret == 0, "ioq_opendir(): %s", xstrerror(errno));
+ }
+ bfs_verify(ioq_capacity(ioq) == 0);
+
+ // Now cancel the queue, pushing an additional IOQ_STOP message
+ ioq_cancel(ioq);
+
+ // Drain the queue
+ for (size_t i = 0; i < depth; ++i) {
+ struct ioq_ent *ent = ioq_pop(ioq, true);
+ bfs_verify(ent && ent->op == IOQ_OPENDIR);
+
+ if (ent->result >= 0) {
+ bfs_closedir(ent->opendir.dir);
+ }
+ free(ent->opendir.dir);
+ ioq_free(ioq, ent);
+ }
+ bfs_verify(!ioq_pop(ioq, true));
+
+ ioq_destroy(ioq);
+}
+
+bool check_ioq(void) {
+ check_ioq_push_block();
+ return true;
+}
diff --git a/tests/ls-color.sh b/tests/ls-color.sh
index c82a58d..b9a0402 100755
--- a/tests/ls-color.sh
+++ b/tests/ls-color.sh
@@ -1,36 +1,50 @@
#!/usr/bin/env bash
-############################################################################
-# bfs #
-# Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> #
-# #
-# Permission to use, copy, modify, and/or distribute this software for any #
-# purpose with or without fee is hereby granted. #
-# #
-# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES #
-# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF #
-# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR #
-# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES #
-# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN #
-# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #
-# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. #
-############################################################################
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
# Prints the "ground truth" coloring of a path using ls
set -e
+parse_ls_colors() {
+ for key; do
+ local -n var="$key"
+ if [[ "$LS_COLORS" =~ (^|:)$key=(([^:]|\\:)*) ]]; then
+ var="${BASH_REMATCH[2]}"
+ # Interpret escapes
+ var=$(printf "$var" | sed $'s/\^\[/\033/g; s/\\\\:/:/g')
+ fi
+ done
+}
+
+re_escape() {
+ # https://stackoverflow.com/a/29613573/502399
+ sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$1"
+}
+
+rs=0
+lc=$'\033['
+rc=m
+ec=
+no=
+
+parse_ls_colors rs lc rc ec no
+: "${ec:=$lc$rs$rc}"
+
+strip="(($(re_escape "$lc$no$rc"))?($(re_escape "$ec")|$(re_escape "$lc$rc")))+"
+
+ls_color() {
+ # Strip the leading reset sequence from the ls output
+ ls -1d --color "$@" | sed -E "s/^$strip([a-z].*)$strip/\4/; s/^$strip//"
+}
+
L=
if [ "$1" = "-L" ]; then
L="$1"
shift
fi
-function ls_color() {
- # Strip the leading reset sequence from the ls output
- ls -1d --color "$@" | sed $'s/^\033\\[0m//'
-}
-
DIR="${1%/*}"
if [ "$DIR" = "$1" ]; then
ls_color "$1"
diff --git a/tests/main.c b/tests/main.c
new file mode 100644
index 0000000..429772b
--- /dev/null
+++ b/tests/main.c
@@ -0,0 +1,125 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Entry point for unit tests.
+ */
+
+#include "prelude.h"
+#include "tests.h"
+#include "bfstd.h"
+#include "color.h"
+#include <errno.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+/**
+ * Test context.
+ */
+struct test_ctx {
+ /** Number of command line arguments. */
+ int argc;
+ /** The arguments themselves. */
+ char **argv;
+
+ /** Parsed colors. */
+ struct colors *colors;
+ /** Colorized output stream. */
+ CFILE *cout;
+
+ /** Eventual exit status. */
+ int ret;
+};
+
+/** Initialize the test context. */
+static int test_init(struct test_ctx *ctx, int argc, char **argv) {
+ ctx->argc = argc;
+ ctx->argv = argv;
+
+ ctx->colors = parse_colors();
+ ctx->cout = cfwrap(stdout, ctx->colors, false);
+ if (!ctx->cout) {
+ ctx->ret = EXIT_FAILURE;
+ return -1;
+ }
+
+ ctx->ret = EXIT_SUCCESS;
+ return 0;
+}
+
+/** Finalize the test context. */
+static int test_fini(struct test_ctx *ctx) {
+ if (ctx->cout) {
+ cfclose(ctx->cout);
+ }
+
+ free_colors(ctx->colors);
+
+ return ctx->ret;
+}
+
+/** Check if a test case is enabled for this run. */
+static bool should_run(const struct test_ctx *ctx, const char *test) {
+ // Run all tests by default
+ if (ctx->argc < 2) {
+ return true;
+ }
+
+ // With args, run only specified tests
+ for (int i = 1; i < ctx->argc; ++i) {
+ if (strcmp(test, ctx->argv[i]) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/** Run a test if it's enabled. */
+static void run_test(struct test_ctx *ctx, const char *test, test_fn *fn) {
+ if (should_run(ctx, test)) {
+ if (fn()) {
+ cfprintf(ctx->cout, "${grn}[PASS]${rs} ${bld}%s${rs}\n", test);
+ } else {
+ cfprintf(ctx->cout, "${red}[FAIL]${rs} ${bld}%s${rs}\n", test);
+ ctx->ret = EXIT_FAILURE;
+ }
+ }
+}
+
+const char *bfs_errstr(void) {
+ return xstrerror(errno);
+}
+
+int main(int argc, char *argv[]) {
+ // Try to set a UTF-8 locale
+ if (!setlocale(LC_ALL, "C.UTF-8")) {
+ setlocale(LC_ALL, "");
+ }
+
+ // Run tests in UTC
+ if (setenv("TZ", "UTC0", true) != 0) {
+ perror("setenv()");
+ return EXIT_FAILURE;
+ }
+ tzset();
+
+ struct test_ctx ctx;
+ if (test_init(&ctx, argc, argv) != 0) {
+ goto done;
+ }
+
+ run_test(&ctx, "alloc", check_alloc);
+ run_test(&ctx, "bfstd", check_bfstd);
+ run_test(&ctx, "bit", check_bit);
+ run_test(&ctx, "ioq", check_ioq);
+ run_test(&ctx, "trie", check_trie);
+ run_test(&ctx, "xspawn", check_xspawn);
+ run_test(&ctx, "xtime", check_xtime);
+
+done:
+ return test_fini(&ctx);
+}
diff --git a/tests/mksock.c b/tests/mksock.c
index d1776b3..5786cb6 100644
--- a/tests/mksock.c
+++ b/tests/mksock.c
@@ -1,29 +1,16 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2019 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
/**
* There's no standard Unix utility that creates a socket file, so this small
* program does the job.
*/
+#include "bfstd.h"
#include <errno.h>
-#include <libgen.h>
#include <stdio.h>
-#include <string.h>
#include <stdlib.h>
+#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
@@ -32,7 +19,7 @@
* Print an error message.
*/
static void errmsg(const char *cmd, const char *path) {
- fprintf(stderr, "%s: '%s': %s.\n", cmd, path, strerror(errno));
+ fprintf(stderr, "%s: '%s': %s.\n", cmd, path, xstrerror(errno));
}
/**
@@ -41,18 +28,13 @@ static void errmsg(const char *cmd, const char *path) {
* file name is not.
*/
static int chdir_parent(const char *path) {
- char *copy = strdup(path);
- if (!copy) {
+ char *dir = xdirname(path);
+ if (!dir) {
return -1;
}
- const char *dir = dirname(copy);
int ret = chdir(dir);
-
- int error = errno;
- free(copy);
- errno = error;
-
+ free(dir);
return ret;
}
@@ -66,22 +48,21 @@ static int init_sun(struct sockaddr_un *sock, const char *path) {
return -1;
}
- char *copy = strdup(path);
- if (!copy) {
+ char *base = xbasename(path);
+ if (!base) {
return -1;
}
- const char *base = basename(copy);
len = strlen(base);
if (len >= sizeof(sock->sun_path)) {
- free(copy);
+ free(base);
errno = ENAMETOOLONG;
return -1;
}
sock->sun_family = AF_UNIX;
memcpy(sock->sun_path, base, len + 1);
- free(copy);
+ free(base);
return 0;
}
@@ -119,7 +100,7 @@ int main(int argc, char *argv[]) {
ret = EXIT_FAILURE;
}
- if (close(fd) != 0) {
+ if (xclose(fd) != 0) {
errmsg(cmd, path);
ret = EXIT_FAILURE;
}
diff --git a/tests/test_P.out b/tests/posix/H.out
index ff635ff..ff635ff 100644
--- a/tests/test_P.out
+++ b/tests/posix/H.out
diff --git a/tests/posix/H.sh b/tests/posix/H.sh
new file mode 100644
index 0000000..5bae1be
--- /dev/null
+++ b/tests/posix/H.sh
@@ -0,0 +1 @@
+bfs_diff -H links/deeply/nested/dir
diff --git a/tests/test_L_samefile_broken.out b/tests/posix/H_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_L_samefile_broken.out
+++ b/tests/posix/H_broken.out
diff --git a/tests/posix/H_broken.sh b/tests/posix/H_broken.sh
new file mode 100644
index 0000000..9ff761c
--- /dev/null
+++ b/tests/posix/H_broken.sh
@@ -0,0 +1 @@
+bfs_diff -H links/broken
diff --git a/tests/test_H_loops.out b/tests/posix/H_loops.out
index 1fc8f8f..1fc8f8f 100644
--- a/tests/test_H_loops.out
+++ b/tests/posix/H_loops.out
diff --git a/tests/posix/H_loops.sh b/tests/posix/H_loops.sh
new file mode 100644
index 0000000..90383b8
--- /dev/null
+++ b/tests/posix/H_loops.sh
@@ -0,0 +1 @@
+bfs_diff -H loops/deeply/nested/loop
diff --git a/tests/test_L_samefile_notdir.out b/tests/posix/H_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_L_samefile_notdir.out
+++ b/tests/posix/H_notdir.out
diff --git a/tests/posix/H_notdir.sh b/tests/posix/H_notdir.sh
new file mode 100644
index 0000000..68d7be7
--- /dev/null
+++ b/tests/posix/H_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -H links/notdir
diff --git a/tests/test_P_slash.out b/tests/posix/H_slash.out
index df7701b..df7701b 100644
--- a/tests/test_P_slash.out
+++ b/tests/posix/H_slash.out
diff --git a/tests/posix/H_slash.sh b/tests/posix/H_slash.sh
new file mode 100644
index 0000000..b44d756
--- /dev/null
+++ b/tests/posix/H_slash.sh
@@ -0,0 +1 @@
+bfs_diff -H links/deeply/nested/dir/
diff --git a/tests/test_path_flag_expr.out b/tests/posix/H_type_l.out
index e67f10b..e67f10b 100644
--- a/tests/test_path_flag_expr.out
+++ b/tests/posix/H_type_l.out
diff --git a/tests/posix/H_type_l.sh b/tests/posix/H_type_l.sh
new file mode 100644
index 0000000..416a53e
--- /dev/null
+++ b/tests/posix/H_type_l.sh
@@ -0,0 +1 @@
+bfs_diff -H links/skip -type l
diff --git a/tests/test_L_depth.out b/tests/posix/L.out
index e24f4f7..ec9e861 100644
--- a/tests/test_L_depth.out
+++ b/tests/posix/L.out
@@ -1,17 +1,17 @@
links
links/broken
links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
links/file
links/hardlink
links/notdir
links/skip
-links/symlink
-links/deeply/nested
links/skip/broken
links/skip/dir
links/skip/file
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/dir
-links/deeply/nested/file
-links/deeply/nested/link
+links/symlink
diff --git a/tests/posix/L.sh b/tests/posix/L.sh
new file mode 100644
index 0000000..d8aebe6
--- /dev/null
+++ b/tests/posix/L.sh
@@ -0,0 +1 @@
+bfs_diff -L links
diff --git a/tests/test_samefile_broken.out b/tests/posix/L_broken.out
index 21d6316..21d6316 100644
--- a/tests/test_samefile_broken.out
+++ b/tests/posix/L_broken.out
diff --git a/tests/posix/L_broken.sh b/tests/posix/L_broken.sh
new file mode 100644
index 0000000..9ff761c
--- /dev/null
+++ b/tests/posix/L_broken.sh
@@ -0,0 +1 @@
+bfs_diff -H links/broken
diff --git a/tests/test_follow.out b/tests/posix/L_depth.out
index e24f4f7..ec9e861 100644
--- a/tests/test_follow.out
+++ b/tests/posix/L_depth.out
@@ -1,17 +1,17 @@
links
links/broken
links/deeply
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/link
links/file
links/hardlink
links/notdir
links/skip
-links/symlink
-links/deeply/nested
links/skip/broken
links/skip/dir
links/skip/file
links/skip/link
-links/deeply/nested/broken
-links/deeply/nested/dir
-links/deeply/nested/file
-links/deeply/nested/link
+links/symlink
diff --git a/tests/posix/L_depth.sh b/tests/posix/L_depth.sh
new file mode 100644
index 0000000..59d7ee9
--- /dev/null
+++ b/tests/posix/L_depth.sh
@@ -0,0 +1 @@
+bfs_diff -L links -depth
diff --git a/tests/posix/L_loops.sh b/tests/posix/L_loops.sh
new file mode 100644
index 0000000..01b7efc
--- /dev/null
+++ b/tests/posix/L_loops.sh
@@ -0,0 +1,4 @@
+# POSIX says it's okay to either stop or keep going on seeing a filesystem
+# loop, as long as a diagnostic is printed
+invoke_bfs -L loops >/dev/null 2>"$OUT" && fail
+test -s "$OUT"
diff --git a/tests/test_samefile_notdir.out b/tests/posix/L_notdir.out
index 6e6658d..6e6658d 100644
--- a/tests/test_samefile_notdir.out
+++ b/tests/posix/L_notdir.out
diff --git a/tests/posix/L_notdir.sh b/tests/posix/L_notdir.sh
new file mode 100644
index 0000000..68d7be7
--- /dev/null
+++ b/tests/posix/L_notdir.sh
@@ -0,0 +1 @@
+bfs_diff -H links/notdir
diff --git a/tests/test_L_type_l.out b/tests/posix/L_type_l.out
index 725d398..725d398 100644
--- a/tests/test_L_type_l.out
+++ b/tests/posix/L_type_l.out
diff --git a/tests/posix/L_type_l.sh b/tests/posix/L_type_l.sh
new file mode 100644
index 0000000..ee9e563
--- /dev/null
+++ b/tests/posix/L_type_l.sh
@@ -0,0 +1 @@
+bfs_diff -L links/skip -type l
diff --git a/tests/posix/L_xdev.out b/tests/posix/L_xdev.out
new file mode 100644
index 0000000..788579d
--- /dev/null
+++ b/tests/posix/L_xdev.out
@@ -0,0 +1,5 @@
+.
+./foo
+./foo/bar
+./foo/qux
+./mnt
diff --git a/tests/posix/L_xdev.sh b/tests/posix/L_xdev.sh
new file mode 100644
index 0000000..82d8605
--- /dev/null
+++ b/tests/posix/L_xdev.sh
@@ -0,0 +1,13 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+ln -s ../mnt foo/bar
+"$XTOUCH" mnt/baz
+ln -s ../mnt/baz foo/qux
+
+bfs_diff -L . -xdev
diff --git a/tests/test_and.out b/tests/posix/a.out
index 722962c..722962c 100644
--- a/tests/test_and.out
+++ b/tests/posix/a.out
diff --git a/tests/posix/a.sh b/tests/posix/a.sh
new file mode 100644
index 0000000..7d82d88
--- /dev/null
+++ b/tests/posix/a.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -a -type d
diff --git a/tests/test_not.out b/tests/posix/bang.out
index 2501b2f..b286454 100644
--- a/tests/test_not.out
+++ b/tests/posix/bang.out
@@ -2,15 +2,15 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/k/foo/bar
+basic/l
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/posix/bang.sh b/tests/posix/bang.sh
new file mode 100644
index 0000000..27840cd
--- /dev/null
+++ b/tests/posix/bang.sh
@@ -0,0 +1 @@
+bfs_diff basic \! -name foo
diff --git a/tests/posix/basic.out b/tests/posix/basic.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/basic.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/basic.sh b/tests/posix/basic.sh
new file mode 100644
index 0000000..3d43529
--- /dev/null
+++ b/tests/posix/basic.sh
@@ -0,0 +1 @@
+bfs_diff basic
diff --git a/tests/test_data_flow_and_swap.out b/tests/posix/data_flow_and_swap.out
index 1e72fd9..e604709 100644
--- a/tests/test_data_flow_and_swap.out
+++ b/tests/posix/data_flow_and_swap.out
@@ -2,11 +2,11 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/g/h
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/data_flow_and_swap.sh b/tests/posix/data_flow_and_swap.sh
new file mode 100644
index 0000000..9a141af
--- /dev/null
+++ b/tests/posix/data_flow_and_swap.sh
@@ -0,0 +1 @@
+bfs_diff basic \! -type f -a -type d
diff --git a/tests/posix/data_flow_group.out b/tests/posix/data_flow_group.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/data_flow_group.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/data_flow_group.sh b/tests/posix/data_flow_group.sh
new file mode 100644
index 0000000..453dc3e
--- /dev/null
+++ b/tests/posix/data_flow_group.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -group "$(id -g)" -nogroup \) -o \( -group "$(id -g)" -o -nogroup \)
diff --git a/tests/test_data_flow_or_swap.out b/tests/posix/data_flow_or_swap.out
index 1e72fd9..e604709 100644
--- a/tests/test_data_flow_or_swap.out
+++ b/tests/posix/data_flow_or_swap.out
@@ -2,11 +2,11 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/g/h
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/data_flow_or_swap.sh b/tests/posix/data_flow_or_swap.sh
new file mode 100644
index 0000000..e8f504b
--- /dev/null
+++ b/tests/posix/data_flow_or_swap.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -type f -o \! -type d \)
diff --git a/tests/test_printf_empty.out b/tests/posix/data_flow_type.out
index e69de29..e69de29 100644
--- a/tests/test_printf_empty.out
+++ b/tests/posix/data_flow_type.out
diff --git a/tests/posix/data_flow_type.sh b/tests/posix/data_flow_type.sh
new file mode 100644
index 0000000..33339df
--- /dev/null
+++ b/tests/posix/data_flow_type.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -type f -o \! -type f \)
diff --git a/tests/posix/data_flow_user.out b/tests/posix/data_flow_user.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/data_flow_user.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/data_flow_user.sh b/tests/posix/data_flow_user.sh
new file mode 100644
index 0000000..44b6e1f
--- /dev/null
+++ b/tests/posix/data_flow_user.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -user "$(id -u)" -nouser \) -o \( -user "$(id -u)" -o -nouser \)
diff --git a/tests/test_de_morgan_and.out b/tests/posix/de_morgan_and.out
index 723790f..7b7afd2 100644
--- a/tests/test_de_morgan_and.out
+++ b/tests/posix/de_morgan_and.out
@@ -2,9 +2,9 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
basic/l
-basic/g/h
basic/l/foo/bar
diff --git a/tests/posix/de_morgan_and.sh b/tests/posix/de_morgan_and.sh
new file mode 100644
index 0000000..d52975e
--- /dev/null
+++ b/tests/posix/de_morgan_and.sh
@@ -0,0 +1 @@
+bfs_diff basic \( \! -name 'foo' -a \! -type f \)
diff --git a/tests/test_de_morgan_not.out b/tests/posix/de_morgan_not.out
index 5916da3..5916da3 100644
--- a/tests/test_de_morgan_not.out
+++ b/tests/posix/de_morgan_not.out
diff --git a/tests/posix/de_morgan_not.sh b/tests/posix/de_morgan_not.sh
new file mode 100644
index 0000000..7393ce0
--- /dev/null
+++ b/tests/posix/de_morgan_not.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -name 'foo' -o \! -type f \)
diff --git a/tests/test_de_morgan_or.out b/tests/posix/de_morgan_or.out
index 697f6b8..2a57066 100644
--- a/tests/test_de_morgan_or.out
+++ b/tests/posix/de_morgan_or.out
@@ -2,17 +2,17 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/posix/de_morgan_or.sh b/tests/posix/de_morgan_or.sh
new file mode 100644
index 0000000..378aab2
--- /dev/null
+++ b/tests/posix/de_morgan_or.sh
@@ -0,0 +1 @@
+bfs_diff basic \( \! -name 'foo' -o \! -type f \)
diff --git a/tests/test_deep_strict.out b/tests/posix/deep.out
index c385fce..c385fce 100644
--- a/tests/test_deep_strict.out
+++ b/tests/posix/deep.out
diff --git a/tests/posix/deep.sh b/tests/posix/deep.sh
new file mode 100644
index 0000000..36a88c0
--- /dev/null
+++ b/tests/posix/deep.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -exec bash -c 'echo "${1:0:6}/.../${1##*/} (${#1})"' bash {} \;
diff --git a/tests/posix/depth.out b/tests/posix/depth.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/depth.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/depth.sh b/tests/posix/depth.sh
new file mode 100644
index 0000000..444eba5
--- /dev/null
+++ b/tests/posix/depth.sh
@@ -0,0 +1 @@
+bfs_diff basic -depth
diff --git a/tests/posix/depth_error.out b/tests/posix/depth_error.out
new file mode 100644
index 0000000..7ed5f0d
--- /dev/null
+++ b/tests/posix/depth_error.out
@@ -0,0 +1,2 @@
+.
+./foo
diff --git a/tests/posix/depth_error.sh b/tests/posix/depth_error.sh
new file mode 100644
index 0000000..db414ba
--- /dev/null
+++ b/tests/posix/depth_error.sh
@@ -0,0 +1,7 @@
+cd "$TEST"
+"$XTOUCH" -p foo/bar
+
+chmod a-r foo
+defer chmod +r foo
+
+! bfs_diff . -depth
diff --git a/tests/test_depth_slash.out b/tests/posix/depth_slash.out
index 7ce7a82..77526d5 100644
--- a/tests/test_depth_slash.out
+++ b/tests/posix/depth_slash.out
@@ -2,18 +2,18 @@ basic/
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
-basic/l/foo
basic/k/foo/bar
+basic/l
+basic/l/foo
basic/l/foo/bar
basic/l/foo/bar/baz
diff --git a/tests/posix/depth_slash.sh b/tests/posix/depth_slash.sh
new file mode 100644
index 0000000..f73e9f1
--- /dev/null
+++ b/tests/posix/depth_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/ -depth
diff --git a/tests/test_double_negation.out b/tests/posix/double_negation.out
index e9d47b1..e9d47b1 100644
--- a/tests/test_double_negation.out
+++ b/tests/posix/double_negation.out
diff --git a/tests/posix/double_negation.sh b/tests/posix/double_negation.sh
new file mode 100644
index 0000000..eefe464
--- /dev/null
+++ b/tests/posix/double_negation.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \! -name 'foo'
diff --git a/tests/posix/exec.out b/tests/posix/exec.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec.sh b/tests/posix/exec.sh
new file mode 100644
index 0000000..96c897b
--- /dev/null
+++ b/tests/posix/exec.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec echo {} \;
diff --git a/tests/posix/exec_nonexistent.out b/tests/posix/exec_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_nonexistent.sh b/tests/posix/exec_nonexistent.sh
new file mode 100644
index 0000000..a9ff052
--- /dev/null
+++ b/tests/posix/exec_nonexistent.sh
@@ -0,0 +1,4 @@
+# Failure to execute the command should lead to an error message and
+# non-zero exit status. See https://unix.stackexchange.com/q/704522/56202
+bfs_diff basic -print -exec "$TESTS/nonexistent" {} \; -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/posix/exec_nopath.out b/tests/posix/exec_nopath.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_nopath.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_nopath.sh b/tests/posix/exec_nopath.sh
new file mode 100644
index 0000000..6e05d2e
--- /dev/null
+++ b/tests/posix/exec_nopath.sh
@@ -0,0 +1,7 @@
+(
+ unset PATH
+ invoke_bfs basic -exec echo {} \; >"$OUT"
+)
+
+sort_output
+diff_output
diff --git a/tests/test_exec_plus.out b/tests/posix/exec_plus.out
index f6b423b..f6b423b 100644
--- a/tests/test_exec_plus.out
+++ b/tests/posix/exec_plus.out
diff --git a/tests/posix/exec_plus.sh b/tests/posix/exec_plus.sh
new file mode 100644
index 0000000..56a93f1
--- /dev/null
+++ b/tests/posix/exec_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -exec "$TESTS/sort-args.sh" {} +
diff --git a/tests/posix/exec_plus_nonexistent.out b/tests/posix/exec_plus_nonexistent.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_plus_nonexistent.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_plus_nonexistent.sh b/tests/posix/exec_plus_nonexistent.sh
new file mode 100644
index 0000000..24582a3
--- /dev/null
+++ b/tests/posix/exec_plus_nonexistent.sh
@@ -0,0 +1,2 @@
+bfs_diff basic -exec "$TESTS/nonexistent" {} + -print 2>"$TEST/err" && fail
+test -s "$TEST/err"
diff --git a/tests/posix/exec_plus_nothing.sh b/tests/posix/exec_plus_nothing.sh
new file mode 100644
index 0000000..347722d
--- /dev/null
+++ b/tests/posix/exec_plus_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't look OOB for {} +
+! invoke_bfs basic -exec +
diff --git a/tests/test_exec_plus_semicolon.out b/tests/posix/exec_plus_semicolon.out
index 3805261..f33c48f 100644
--- a/tests/test_exec_plus_semicolon.out
+++ b/tests/posix/exec_plus_semicolon.out
@@ -2,18 +2,18 @@ foo basic bar + baz
foo basic/a bar + baz
foo basic/b bar + baz
foo basic/c bar + baz
+foo basic/c/d bar + baz
foo basic/e bar + baz
+foo basic/e/f bar + baz
foo basic/g bar + baz
+foo basic/g/h bar + baz
foo basic/i bar + baz
foo basic/j bar + baz
-foo basic/k bar + baz
-foo basic/l bar + baz
-foo basic/c/d bar + baz
-foo basic/e/f bar + baz
-foo basic/g/h bar + baz
foo basic/j/foo bar + baz
+foo basic/k bar + baz
foo basic/k/foo bar + baz
-foo basic/l/foo bar + baz
foo basic/k/foo/bar bar + baz
+foo basic/l bar + baz
+foo basic/l/foo bar + baz
foo basic/l/foo/bar bar + baz
foo basic/l/foo/bar/baz bar + baz
diff --git a/tests/posix/exec_plus_semicolon.sh b/tests/posix/exec_plus_semicolon.sh
new file mode 100644
index 0000000..449a3f9
--- /dev/null
+++ b/tests/posix/exec_plus_semicolon.sh
@@ -0,0 +1,5 @@
+# POSIX says:
+# Only a <plus-sign> that immediately follows an argument containing only the two characters "{}"
+# shall punctuate the end of the primary expression. Other uses of the <plus-sign> shall not be
+# treated as special.
+bfs_diff basic -exec echo foo {} bar + baz \;
diff --git a/tests/posix/exec_plus_status.out b/tests/posix/exec_plus_status.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/exec_plus_status.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/exec_plus_status.sh b/tests/posix/exec_plus_status.sh
new file mode 100644
index 0000000..a814c4e
--- /dev/null
+++ b/tests/posix/exec_plus_status.sh
@@ -0,0 +1,3 @@
+# -exec ... {} + should always return true, but if the command fails, bfs
+# should exit with a non-zero status
+! bfs_diff basic -exec false {} + -print
diff --git a/tests/posix/exec_ulimit.out b/tests/posix/exec_ulimit.out
new file mode 100644
index 0000000..144169e
--- /dev/null
+++ b/tests/posix/exec_ulimit.out
@@ -0,0 +1,16 @@
+64 deep/0/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/1/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/2/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/3/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/4/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/5/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/6/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/7/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/8/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/9/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/A/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/B/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/C/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/D/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/E/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
+64 deep/F/.../0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDE
diff --git a/tests/posix/exec_ulimit.sh b/tests/posix/exec_ulimit.sh
new file mode 100644
index 0000000..655fbec
--- /dev/null
+++ b/tests/posix/exec_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -Sn 64
+bfs_diff deep -type f -exec bash -c 'printf "%d %s\n" $(ulimit -Sn) "${1:0:6}/.../${1##*/}"' bash {} \;
diff --git a/tests/posix/extra_paren.sh b/tests/posix/extra_paren.sh
new file mode 100644
index 0000000..d15022f
--- /dev/null
+++ b/tests/posix/extra_paren.sh
@@ -0,0 +1 @@
+! invoke_bfs basic -print \)
diff --git a/tests/test_flag_comma.out b/tests/posix/flag_comma.out
index 3574388..3574388 100644
--- a/tests/test_flag_comma.out
+++ b/tests/posix/flag_comma.out
diff --git a/tests/posix/flag_comma.sh b/tests/posix/flag_comma.sh
new file mode 100644
index 0000000..cec87e7
--- /dev/null
+++ b/tests/posix/flag_comma.sh
@@ -0,0 +1,3 @@
+# , is a filename until a non-flag is seen
+cd weirdnames
+bfs_diff -L ',' -print
diff --git a/tests/test_flag_weird_names.out b/tests/posix/flag_weird_names.out
index 73d0ff3..c395659 100644
--- a/tests/test_flag_weird_names.out
+++ b/tests/posix/flag_weird_names.out
@@ -1,28 +1,28 @@
!-
!-
-(-
-(-
-)
-)
-,
-,
--
--
!-/e
!-/e
+(-
+(-
(-/c
(-/c
+)
+)
)/g
)/g
+,
+,
,/f
,/f
+-
+-
-/a
-/a
./!
./!
-./(
-./(
./!/d
./!/d
+./(
+./(
./(/b
./(/b
diff --git a/tests/posix/flag_weird_names.sh b/tests/posix/flag_weird_names.sh
new file mode 100644
index 0000000..f6596e9
--- /dev/null
+++ b/tests/posix/flag_weird_names.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff -L '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
diff --git a/tests/posix/group_id.out b/tests/posix/group_id.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_id.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_id.sh b/tests/posix/group_id.sh
new file mode 100644
index 0000000..2ff7bb3
--- /dev/null
+++ b/tests/posix/group_id.sh
@@ -0,0 +1 @@
+bfs_diff basic -group "$(id -g)"
diff --git a/tests/posix/group_name.out b/tests/posix/group_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_name.sh b/tests/posix/group_name.sh
new file mode 100644
index 0000000..36799d9
--- /dev/null
+++ b/tests/posix/group_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -group "$(id -gn)"
diff --git a/tests/posix/group_nogroup.out b/tests/posix/group_nogroup.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_nogroup.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_nogroup.sh b/tests/posix/group_nogroup.sh
new file mode 100644
index 0000000..cbd1ffc
--- /dev/null
+++ b/tests/posix/group_nogroup.sh
@@ -0,0 +1,2 @@
+# Regression test: this was wrongly optimized to -false
+bfs_diff basic -group "$(id -g)" \! -nogroup
diff --git a/tests/test_implicit_and.out b/tests/posix/implicit_and.out
index 722962c..722962c 100644
--- a/tests/test_implicit_and.out
+++ b/tests/posix/implicit_and.out
diff --git a/tests/posix/implicit_and.sh b/tests/posix/implicit_and.sh
new file mode 100644
index 0000000..161ab0b
--- /dev/null
+++ b/tests/posix/implicit_and.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -type d
diff --git a/tests/posix/incomplete.sh b/tests/posix/incomplete.sh
new file mode 100644
index 0000000..bca5a13
--- /dev/null
+++ b/tests/posix/incomplete.sh
@@ -0,0 +1 @@
+! invoke_bfs basic \(
diff --git a/tests/test_links_plus.out b/tests/posix/links.out
index 996ffc8..996ffc8 100644
--- a/tests/test_links_plus.out
+++ b/tests/posix/links.out
diff --git a/tests/posix/links.sh b/tests/posix/links.sh
new file mode 100644
index 0000000..3d8ad80
--- /dev/null
+++ b/tests/posix/links.sh
@@ -0,0 +1 @@
+bfs_diff links -type f -links 2
diff --git a/tests/test_links_minus.out b/tests/posix/links_minus.out
index eda26f1..eda26f1 100644
--- a/tests/test_links_minus.out
+++ b/tests/posix/links_minus.out
diff --git a/tests/posix/links_minus.sh b/tests/posix/links_minus.sh
new file mode 100644
index 0000000..3ee0803
--- /dev/null
+++ b/tests/posix/links_minus.sh
@@ -0,0 +1 @@
+bfs_diff links -type f -links -2
diff --git a/tests/test_samefile.out b/tests/posix/links_plus.out
index 996ffc8..996ffc8 100644
--- a/tests/test_samefile.out
+++ b/tests/posix/links_plus.out
diff --git a/tests/posix/links_plus.sh b/tests/posix/links_plus.sh
new file mode 100644
index 0000000..375834b
--- /dev/null
+++ b/tests/posix/links_plus.sh
@@ -0,0 +1 @@
+bfs_diff links -type f -links +1
diff --git a/tests/posix/missing_paren.sh b/tests/posix/missing_paren.sh
new file mode 100644
index 0000000..d906fbe
--- /dev/null
+++ b/tests/posix/missing_paren.sh
@@ -0,0 +1 @@
+! invoke_bfs basic \( -print
diff --git a/tests/test_parens.out b/tests/posix/name.out
index a9e5d42..a9e5d42 100644
--- a/tests/test_parens.out
+++ b/tests/posix/name.out
diff --git a/tests/posix/name.sh b/tests/posix/name.sh
new file mode 100644
index 0000000..a673ad0
--- /dev/null
+++ b/tests/posix/name.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '*f*'
diff --git a/tests/test_printf_w.out b/tests/posix/name_backslash.out
index e69de29..e69de29 100644
--- a/tests/test_printf_w.out
+++ b/tests/posix/name_backslash.out
diff --git a/tests/posix/name_backslash.sh b/tests/posix/name_backslash.sh
new file mode 100644
index 0000000..ff9b539
--- /dev/null
+++ b/tests/posix/name_backslash.sh
@@ -0,0 +1,2 @@
+# An unescaped \ doesn't match
+bfs_diff weirdnames -name '\'
diff --git a/tests/posix/name_bracket.out b/tests/posix/name_bracket.out
new file mode 100644
index 0000000..5ff3c0c
--- /dev/null
+++ b/tests/posix/name_bracket.out
@@ -0,0 +1 @@
+weirdnames/[
diff --git a/tests/posix/name_bracket.sh b/tests/posix/name_bracket.sh
new file mode 100644
index 0000000..e2f943d
--- /dev/null
+++ b/tests/posix/name_bracket.sh
@@ -0,0 +1,9 @@
+# fnmatch() is broken on some platforms
+case "$UNAME" in
+ Darwin|NetBSD)
+ skip
+ ;;
+esac
+
+# An unclosed [ should be matched literally
+bfs_diff weirdnames -name '['
diff --git a/tests/test_prune.out b/tests/posix/name_character_class.out
index e9d47b1..e9d47b1 100644
--- a/tests/test_prune.out
+++ b/tests/posix/name_character_class.out
diff --git a/tests/posix/name_character_class.sh b/tests/posix/name_character_class.sh
new file mode 100644
index 0000000..ecda190
--- /dev/null
+++ b/tests/posix/name_character_class.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '[e-g][!a-n][!p-z]'
diff --git a/tests/posix/name_double_backslash.out b/tests/posix/name_double_backslash.out
new file mode 100644
index 0000000..45ceda0
--- /dev/null
+++ b/tests/posix/name_double_backslash.out
@@ -0,0 +1 @@
+weirdnames/\
diff --git a/tests/posix/name_double_backslash.sh b/tests/posix/name_double_backslash.sh
new file mode 100644
index 0000000..009553a
--- /dev/null
+++ b/tests/posix/name_double_backslash.sh
@@ -0,0 +1,2 @@
+# An escaped \\ matches
+bfs_diff weirdnames -name '\\'
diff --git a/tests/posix/name_root.out b/tests/posix/name_root.out
new file mode 100644
index 0000000..511198f
--- /dev/null
+++ b/tests/posix/name_root.out
@@ -0,0 +1 @@
+basic/a
diff --git a/tests/posix/name_root.sh b/tests/posix/name_root.sh
new file mode 100644
index 0000000..785861e
--- /dev/null
+++ b/tests/posix/name_root.sh
@@ -0,0 +1 @@
+bfs_diff basic/a -name a
diff --git a/tests/test_quit.out b/tests/posix/name_root_depth.out
index cf4d5a9..cf4d5a9 100644
--- a/tests/test_quit.out
+++ b/tests/posix/name_root_depth.out
diff --git a/tests/posix/name_root_depth.sh b/tests/posix/name_root_depth.sh
new file mode 100644
index 0000000..dc3b8bb
--- /dev/null
+++ b/tests/posix/name_root_depth.sh
@@ -0,0 +1 @@
+bfs_diff basic/g -depth -name g
diff --git a/tests/posix/name_star_star.out b/tests/posix/name_star_star.out
new file mode 100644
index 0000000..a9e5d42
--- /dev/null
+++ b/tests/posix/name_star_star.out
@@ -0,0 +1,4 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/l/foo
diff --git a/tests/posix/name_star_star.sh b/tests/posix/name_star_star.sh
new file mode 100644
index 0000000..035f635
--- /dev/null
+++ b/tests/posix/name_star_star.sh
@@ -0,0 +1 @@
+bfs_diff basic -name '**f**'
diff --git a/tests/test_name_trailing_slash.out b/tests/posix/name_trailing_slash.out
index daff2f5..daff2f5 100644
--- a/tests/test_name_trailing_slash.out
+++ b/tests/posix/name_trailing_slash.out
diff --git a/tests/posix/name_trailing_slash.sh b/tests/posix/name_trailing_slash.sh
new file mode 100644
index 0000000..ab058d1
--- /dev/null
+++ b/tests/posix/name_trailing_slash.sh
@@ -0,0 +1 @@
+bfs_diff basic/g/ -name g
diff --git a/tests/test_newerma.out b/tests/posix/newer.out
index 7f6c0dd..7f6c0dd 100644
--- a/tests/test_newerma.out
+++ b/tests/posix/newer.out
diff --git a/tests/posix/newer.sh b/tests/posix/newer.sh
new file mode 100644
index 0000000..860623a
--- /dev/null
+++ b/tests/posix/newer.sh
@@ -0,0 +1 @@
+bfs_diff times -newer times/a
diff --git a/tests/posix/newer_broken.out b/tests/posix/newer_broken.out
new file mode 100644
index 0000000..d2dcdd1
--- /dev/null
+++ b/tests/posix/newer_broken.out
@@ -0,0 +1 @@
+times
diff --git a/tests/posix/newer_broken.sh b/tests/posix/newer_broken.sh
new file mode 100644
index 0000000..dccaa73
--- /dev/null
+++ b/tests/posix/newer_broken.sh
@@ -0,0 +1,4 @@
+ln -s nowhere "$TEST/broken"
+"$XTOUCH" -h -t "1991-12-14 00:03" "$TEST/broken"
+
+bfs_diff times -newer "$TEST/broken"
diff --git a/tests/posix/newer_nonexistent.sh b/tests/posix/newer_nonexistent.sh
new file mode 100644
index 0000000..5f2da4b
--- /dev/null
+++ b/tests/posix/newer_nonexistent.sh
@@ -0,0 +1 @@
+! invoke_bfs times -newer times/nonexistent
diff --git a/tests/test_quit_before_print.out b/tests/posix/nogroup.out
index e69de29..e69de29 100644
--- a/tests/test_quit_before_print.out
+++ b/tests/posix/nogroup.out
diff --git a/tests/posix/nogroup.sh b/tests/posix/nogroup.sh
new file mode 100644
index 0000000..60ffd68
--- /dev/null
+++ b/tests/posix/nogroup.sh
@@ -0,0 +1 @@
+bfs_diff basic -nogroup
diff --git a/tests/test_size_big.out b/tests/posix/nogroup_ulimit.out
index e69de29..e69de29 100644
--- a/tests/test_size_big.out
+++ b/tests/posix/nogroup_ulimit.out
diff --git a/tests/posix/nogroup_ulimit.sh b/tests/posix/nogroup_ulimit.sh
new file mode 100644
index 0000000..a39dd1f
--- /dev/null
+++ b/tests/posix/nogroup_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -nogroup
diff --git a/tests/test_exclude_depth.out b/tests/posix/not_prune.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_exclude_depth.out
+++ b/tests/posix/not_prune.out
@@ -2,12 +2,12 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
basic/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/posix/not_prune.sh b/tests/posix/not_prune.sh
new file mode 100644
index 0000000..6d7b092
--- /dev/null
+++ b/tests/posix/not_prune.sh
@@ -0,0 +1 @@
+bfs_diff basic \! \( -name foo -prune \)
diff --git a/tests/test_xtype_reorder.out b/tests/posix/nouser.out
index e69de29..e69de29 100644
--- a/tests/test_xtype_reorder.out
+++ b/tests/posix/nouser.out
diff --git a/tests/posix/nouser.sh b/tests/posix/nouser.sh
new file mode 100644
index 0000000..e7c48c0
--- /dev/null
+++ b/tests/posix/nouser.sh
@@ -0,0 +1 @@
+bfs_diff basic -nouser
diff --git a/tests/posix/nouser_ulimit.out b/tests/posix/nouser_ulimit.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/nouser_ulimit.out
diff --git a/tests/posix/nouser_ulimit.sh b/tests/posix/nouser_ulimit.sh
new file mode 100644
index 0000000..a94b8c5
--- /dev/null
+++ b/tests/posix/nouser_ulimit.sh
@@ -0,0 +1,2 @@
+ulimit -n $((NOPENFD + 13))
+bfs_diff deep -type f -nouser
diff --git a/tests/test_or.out b/tests/posix/o.out
index 9a82ee2..1650c4d 100644
--- a/tests/test_or.out
+++ b/tests/posix/o.out
@@ -2,12 +2,12 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
-basic/k
-basic/l
-basic/g/h
basic/j/foo
+basic/k
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/o.sh b/tests/posix/o.sh
new file mode 100644
index 0000000..6dcd442
--- /dev/null
+++ b/tests/posix/o.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -o -type d
diff --git a/tests/posix/ok_plus_nothing.sh b/tests/posix/ok_plus_nothing.sh
new file mode 100644
index 0000000..77c7644
--- /dev/null
+++ b/tests/posix/ok_plus_nothing.sh
@@ -0,0 +1,2 @@
+# Regression test: don't look OOB for {} +
+! invoke_bfs basic -ok +
diff --git a/tests/test_ok_stdin.out b/tests/posix/ok_stdin.out
index 30b57fd..7acf711 100644
--- a/tests/test_ok_stdin.out
+++ b/tests/posix/ok_stdin.out
@@ -1,19 +1,19 @@
-basic? y
basic/a? y
basic/b? y
+basic/c/d? y
basic/c? y
+basic/e/f? y
basic/e? y
+basic/g/h? y
basic/g? y
basic/i? y
-basic/j? y
-basic/k? y
-basic/l? y
-basic/c/d? y
-basic/e/f? y
-basic/g/h? y
basic/j/foo? y
-basic/k/foo? y
-basic/l/foo? y
+basic/j? y
basic/k/foo/bar? y
-basic/l/foo/bar? y
+basic/k/foo? y
+basic/k? y
basic/l/foo/bar/baz? y
+basic/l/foo/bar? y
+basic/l/foo? y
+basic/l? y
+basic? y
diff --git a/tests/posix/ok_stdin.sh b/tests/posix/ok_stdin.sh
new file mode 100644
index 0000000..a190d81
--- /dev/null
+++ b/tests/posix/ok_stdin.sh
@@ -0,0 +1,3 @@
+# -ok should *not* close stdin
+# See https://savannah.gnu.org/bugs/?24561
+yes | bfs_diff basic -ok bash -c 'printf "%s? " "$1" && head -n1' bash {} \;
diff --git a/tests/posix/or_purity.out b/tests/posix/or_purity.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/or_purity.out
diff --git a/tests/posix/or_purity.sh b/tests/posix/or_purity.sh
new file mode 100644
index 0000000..277b18b
--- /dev/null
+++ b/tests/posix/or_purity.sh
@@ -0,0 +1,2 @@
+# Regression test: (-o lhs(pure) rhs(always_true)) <==> rhs is only valid if rhs is pure
+bfs_diff basic -name '*' -o -print
diff --git a/tests/posix/overlayfs.out b/tests/posix/overlayfs.out
new file mode 100644
index 0000000..b472b56
--- /dev/null
+++ b/tests/posix/overlayfs.out
@@ -0,0 +1,5 @@
+merged
+merged/bar
+merged/baz
+merged/baz/qux
+merged/foo
diff --git a/tests/posix/overlayfs.sh b/tests/posix/overlayfs.sh
new file mode 100644
index 0000000..21ef22f
--- /dev/null
+++ b/tests/posix/overlayfs.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux}
+
+mkdir -p work merged
+bfs_sudo mount -t overlay overlay -olowerdir=lower,upperdir=upper,workdir=work merged || skip
+defer bfs_sudo rm -rf work
+defer bfs_sudo umount merged
+
+bfs_diff merged
diff --git a/tests/posix/parens.out b/tests/posix/parens.out
new file mode 100644
index 0000000..a9e5d42
--- /dev/null
+++ b/tests/posix/parens.out
@@ -0,0 +1,4 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/l/foo
diff --git a/tests/posix/parens.sh b/tests/posix/parens.sh
new file mode 100644
index 0000000..abbb20f
--- /dev/null
+++ b/tests/posix/parens.sh
@@ -0,0 +1 @@
+bfs_diff basic \( -name '*f*' \)
diff --git a/tests/posix/path.out b/tests/posix/path.out
new file mode 100644
index 0000000..ae1ae21
--- /dev/null
+++ b/tests/posix/path.out
@@ -0,0 +1,7 @@
+basic/e/f
+basic/j/foo
+basic/k/foo
+basic/k/foo/bar
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/path.sh b/tests/posix/path.sh
new file mode 100644
index 0000000..04606eb
--- /dev/null
+++ b/tests/posix/path.sh
@@ -0,0 +1 @@
+bfs_diff basic -path 'basic/*f*'
diff --git a/tests/test_perm_000.out b/tests/posix/perm_000.out
index 5fd30bc..5fd30bc 100644
--- a/tests/test_perm_000.out
+++ b/tests/posix/perm_000.out
diff --git a/tests/posix/perm_000.sh b/tests/posix/perm_000.sh
new file mode 100644
index 0000000..ee25f23
--- /dev/null
+++ b/tests/posix/perm_000.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm 000
diff --git a/tests/test_perm_000_slash.out b/tests/posix/perm_000_minus.out
index d7494b8..d7494b8 100644
--- a/tests/test_perm_000_slash.out
+++ b/tests/posix/perm_000_minus.out
diff --git a/tests/posix/perm_000_minus.sh b/tests/posix/perm_000_minus.sh
new file mode 100644
index 0000000..5027b91
--- /dev/null
+++ b/tests/posix/perm_000_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -000
diff --git a/tests/test_perm_222.out b/tests/posix/perm_222.out
index 1690e43..1690e43 100644
--- a/tests/test_perm_222.out
+++ b/tests/posix/perm_222.out
diff --git a/tests/posix/perm_222.sh b/tests/posix/perm_222.sh
new file mode 100644
index 0000000..40f5804
--- /dev/null
+++ b/tests/posix/perm_222.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm 222
diff --git a/tests/test_perm_222_minus.out b/tests/posix/perm_222_minus.out
index 1690e43..1690e43 100644
--- a/tests/test_perm_222_minus.out
+++ b/tests/posix/perm_222_minus.out
diff --git a/tests/posix/perm_222_minus.sh b/tests/posix/perm_222_minus.sh
new file mode 100644
index 0000000..4e7ad5a
--- /dev/null
+++ b/tests/posix/perm_222_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -222
diff --git a/tests/test_perm_644.out b/tests/posix/perm_644.out
index 4e64e49..4e64e49 100644
--- a/tests/test_perm_644.out
+++ b/tests/posix/perm_644.out
diff --git a/tests/posix/perm_644.sh b/tests/posix/perm_644.sh
new file mode 100644
index 0000000..9a4f41d
--- /dev/null
+++ b/tests/posix/perm_644.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm 644
diff --git a/tests/test_perm_644_minus.out b/tests/posix/perm_644_minus.out
index 2e2576b..2e2576b 100644
--- a/tests/test_perm_644_minus.out
+++ b/tests/posix/perm_644_minus.out
diff --git a/tests/posix/perm_644_minus.sh b/tests/posix/perm_644_minus.sh
new file mode 100644
index 0000000..6464f84
--- /dev/null
+++ b/tests/posix/perm_644_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -644
diff --git a/tests/posix/perm_leading_plus_symbolic_minus.out b/tests/posix/perm_leading_plus_symbolic_minus.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/perm_leading_plus_symbolic_minus.out
diff --git a/tests/posix/perm_leading_plus_symbolic_minus.sh b/tests/posix/perm_leading_plus_symbolic_minus.sh
new file mode 100644
index 0000000..60389c0
--- /dev/null
+++ b/tests/posix/perm_leading_plus_symbolic_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -+rwx
diff --git a/tests/test_perm_setid.out b/tests/posix/perm_setid.out
index 865a74e..865a74e 100644
--- a/tests/test_perm_setid.out
+++ b/tests/posix/perm_setid.out
diff --git a/tests/posix/perm_setid.sh b/tests/posix/perm_setid.sh
new file mode 100644
index 0000000..3b98647
--- /dev/null
+++ b/tests/posix/perm_setid.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -perm -u+s -o -perm -g+s
diff --git a/tests/test_perm_sticky.out b/tests/posix/perm_sticky.out
index c07eb61..c07eb61 100644
--- a/tests/test_perm_sticky.out
+++ b/tests/posix/perm_sticky.out
diff --git a/tests/posix/perm_sticky.sh b/tests/posix/perm_sticky.sh
new file mode 100644
index 0000000..6bdf8e9
--- /dev/null
+++ b/tests/posix/perm_sticky.sh
@@ -0,0 +1 @@
+bfs_diff rainbow -perm -a+t
diff --git a/tests/posix/perm_symbolic.out b/tests/posix/perm_symbolic.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/posix/perm_symbolic.out
diff --git a/tests/posix/perm_symbolic.sh b/tests/posix/perm_symbolic.sh
new file mode 100644
index 0000000..5cfddb6
--- /dev/null
+++ b/tests/posix/perm_symbolic.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm a+r,u=wX,g+wX-w
diff --git a/tests/test_perm_symbolic_minus.out b/tests/posix/perm_symbolic_minus.out
index 2e2576b..2e2576b 100644
--- a/tests/test_perm_symbolic_minus.out
+++ b/tests/posix/perm_symbolic_minus.out
diff --git a/tests/posix/perm_symbolic_minus.sh b/tests/posix/perm_symbolic_minus.sh
new file mode 100644
index 0000000..b6ba3a5
--- /dev/null
+++ b/tests/posix/perm_symbolic_minus.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm -a+r,u=wX,g+wX-w
diff --git a/tests/test_permcopy.out b/tests/posix/permcopy.out
index 4e64e49..4e64e49 100644
--- a/tests/test_permcopy.out
+++ b/tests/posix/permcopy.out
diff --git a/tests/posix/permcopy.sh b/tests/posix/permcopy.sh
new file mode 100644
index 0000000..3c85cce
--- /dev/null
+++ b/tests/posix/permcopy.sh
@@ -0,0 +1 @@
+bfs_diff perms -perm u+rw,g+u-w,o=g
diff --git a/tests/posix/prune.out b/tests/posix/prune.out
new file mode 100644
index 0000000..e9d47b1
--- /dev/null
+++ b/tests/posix/prune.out
@@ -0,0 +1,3 @@
+basic/j/foo
+basic/k/foo
+basic/l/foo
diff --git a/tests/posix/prune.sh b/tests/posix/prune.sh
new file mode 100644
index 0000000..b48ab48
--- /dev/null
+++ b/tests/posix/prune.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -prune
diff --git a/tests/posix/prune_file.out b/tests/posix/prune_file.out
new file mode 100644
index 0000000..7575ae4
--- /dev/null
+++ b/tests/posix/prune_file.out
@@ -0,0 +1,10 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/e
+basic/g
+basic/i
+basic/j
+basic/k
+basic/l
diff --git a/tests/posix/prune_file.sh b/tests/posix/prune_file.sh
new file mode 100644
index 0000000..29a3a33
--- /dev/null
+++ b/tests/posix/prune_file.sh
@@ -0,0 +1 @@
+bfs_diff basic -print -name '?' -prune
diff --git a/tests/test_exclude_name.out b/tests/posix/prune_or_print.out
index 40e2ea0..59e3c42 100644
--- a/tests/test_exclude_name.out
+++ b/tests/posix/prune_or_print.out
@@ -2,12 +2,12 @@ basic
basic/a
basic/b
basic/c
+basic/c/d
basic/e
+basic/e/f
basic/g
+basic/g/h
basic/i
basic/j
basic/k
basic/l
-basic/c/d
-basic/e/f
-basic/g/h
diff --git a/tests/posix/prune_or_print.sh b/tests/posix/prune_or_print.sh
new file mode 100644
index 0000000..85b97fd
--- /dev/null
+++ b/tests/posix/prune_or_print.sh
@@ -0,0 +1 @@
+bfs_diff basic -name foo -prune -o -print
diff --git a/tests/posix/readdir_error.sh b/tests/posix/readdir_error.sh
new file mode 100644
index 0000000..82fcd17
--- /dev/null
+++ b/tests/posix/readdir_error.sh
@@ -0,0 +1,37 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+mkfifo hang pid wait running
+
+(
+ # Create a zombie process
+ cat hang >/dev/null &
+ # Write the PID to pid
+ echo $! >pid
+ # Don't wait on the zombie process
+ exec cat wait hang >running
+) &
+
+# Kill the parent cat on exit
+defer kill -9 %1
+
+# Read the child PID
+read -r pid <pid
+
+# Make sure the parent cat is running before we kill the child, because bash
+# will wait() on its children
+echo >wait &
+read -r _ <running
+
+# Turn the child into a zombie
+kill -9 "$pid"
+
+# Wait until it's really a zombie
+state=R
+while [ "$state" != "Z" ]; do
+ read -r _ _ state _ <"/proc/$pid/stat"
+done
+
+# On Linux, open(/proc/$pid/net) will succeed but readdir() will fail
+test -r "/proc/$pid/net" || skip
+! invoke_bfs "/proc/$pid/net" >/dev/null
diff --git a/tests/test_size.out b/tests/posix/size.out
index eeabbd7..eeabbd7 100644
--- a/tests/test_size.out
+++ b/tests/posix/size.out
diff --git a/tests/posix/size.sh b/tests/posix/size.sh
new file mode 100644
index 0000000..1e7528a
--- /dev/null
+++ b/tests/posix/size.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size 0
diff --git a/tests/test_size_bytes.out b/tests/posix/size_bytes.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_bytes.out
+++ b/tests/posix/size_bytes.out
diff --git a/tests/posix/size_bytes.sh b/tests/posix/size_bytes.sh
new file mode 100644
index 0000000..6a68321
--- /dev/null
+++ b/tests/posix/size_bytes.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size +0c
diff --git a/tests/test_size_plus.out b/tests/posix/size_plus.out
index 279f3f1..279f3f1 100644
--- a/tests/test_size_plus.out
+++ b/tests/posix/size_plus.out
diff --git a/tests/posix/size_plus.sh b/tests/posix/size_plus.sh
new file mode 100644
index 0000000..01853d5
--- /dev/null
+++ b/tests/posix/size_plus.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f -size +0
diff --git a/tests/posix/type_bind_mount.out b/tests/posix/type_bind_mount.out
new file mode 100644
index 0000000..2f06c47
--- /dev/null
+++ b/tests/posix/type_bind_mount.out
@@ -0,0 +1 @@
+./null
diff --git a/tests/posix/type_bind_mount.sh b/tests/posix/type_bind_mount.sh
new file mode 100644
index 0000000..97b7305
--- /dev/null
+++ b/tests/posix/type_bind_mount.sh
@@ -0,0 +1,9 @@
+test "$UNAME" = "Linux" || skip
+
+cd "$TEST"
+"$XTOUCH" file null
+
+bfs_sudo mount --bind /dev/null null || skip
+defer bfs_sudo umount null
+
+bfs_diff . -type c
diff --git a/tests/test_type_d.out b/tests/posix/type_d.out
index 1e72fd9..e604709 100644
--- a/tests/test_type_d.out
+++ b/tests/posix/type_d.out
@@ -2,11 +2,11 @@ basic
basic/c
basic/e
basic/g
+basic/g/h
basic/i
basic/j
basic/k
-basic/l
-basic/g/h
basic/k/foo
+basic/l
basic/l/foo
basic/l/foo/bar
diff --git a/tests/posix/type_d.sh b/tests/posix/type_d.sh
new file mode 100644
index 0000000..8d06b73
--- /dev/null
+++ b/tests/posix/type_d.sh
@@ -0,0 +1 @@
+bfs_diff basic -type d
diff --git a/tests/posix/type_f.out b/tests/posix/type_f.out
new file mode 100644
index 0000000..6218a0c
--- /dev/null
+++ b/tests/posix/type_f.out
@@ -0,0 +1,7 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
+basic/j/foo
+basic/k/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/type_f.sh b/tests/posix/type_f.sh
new file mode 100644
index 0000000..1fd0c8c
--- /dev/null
+++ b/tests/posix/type_f.sh
@@ -0,0 +1 @@
+bfs_diff basic -type f
diff --git a/tests/test_type_l.out b/tests/posix/type_l.out
index f2c8b19..f2c8b19 100644
--- a/tests/test_type_l.out
+++ b/tests/posix/type_l.out
diff --git a/tests/posix/type_l.sh b/tests/posix/type_l.sh
new file mode 100644
index 0000000..457f74d
--- /dev/null
+++ b/tests/posix/type_l.sh
@@ -0,0 +1 @@
+bfs_diff links/skip -type l
diff --git a/tests/posix/unionfs.out b/tests/posix/unionfs.out
new file mode 100644
index 0000000..28c4ec1
--- /dev/null
+++ b/tests/posix/unionfs.out
@@ -0,0 +1,10 @@
+.
+./lower
+./lower/bar
+./lower/baz
+./lower/foo
+./upper
+./upper/bar
+./upper/baz
+./upper/baz/qux
+./upper/foo
diff --git a/tests/posix/unionfs.sh b/tests/posix/unionfs.sh
new file mode 100644
index 0000000..94d3929
--- /dev/null
+++ b/tests/posix/unionfs.sh
@@ -0,0 +1,9 @@
+[[ "$UNAME" == *BSD* ]] || skip
+
+cd "$TEST"
+"$XTOUCH" -p lower/{foo,bar,baz} upper/{bar,baz/qux}
+
+bfs_sudo mount -t unionfs -o below lower upper || skip
+defer bfs_sudo umount upper
+
+bfs_diff .
diff --git a/tests/posix/user_id.out b/tests/posix/user_id.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_id.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_id.sh b/tests/posix/user_id.sh
new file mode 100644
index 0000000..c3e4b31
--- /dev/null
+++ b/tests/posix/user_id.sh
@@ -0,0 +1 @@
+bfs_diff basic -user "$(id -u)"
diff --git a/tests/posix/user_name.out b/tests/posix/user_name.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_name.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_name.sh b/tests/posix/user_name.sh
new file mode 100644
index 0000000..8599249
--- /dev/null
+++ b/tests/posix/user_name.sh
@@ -0,0 +1 @@
+bfs_diff basic -user "$(id -un)"
diff --git a/tests/posix/user_nouser.out b/tests/posix/user_nouser.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_nouser.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_nouser.sh b/tests/posix/user_nouser.sh
new file mode 100644
index 0000000..e72bd45
--- /dev/null
+++ b/tests/posix/user_nouser.sh
@@ -0,0 +1,2 @@
+# Regression test: this was wrongly optimized to -false
+bfs_diff basic -user "$(id -u)" \! -nouser
diff --git a/tests/test_weird_names.out b/tests/posix/weird_names.out
index 73d0ff3..c395659 100644
--- a/tests/test_weird_names.out
+++ b/tests/posix/weird_names.out
@@ -1,28 +1,28 @@
!-
!-
-(-
-(-
-)
-)
-,
-,
--
--
!-/e
!-/e
+(-
+(-
(-/c
(-/c
+)
+)
)/g
)/g
+,
+,
,/f
,/f
+-
+-
-/a
-/a
./!
./!
-./(
-./(
./!/d
./!/d
+./(
+./(
./(/b
./(/b
diff --git a/tests/posix/weird_names.sh b/tests/posix/weird_names.sh
new file mode 100644
index 0000000..8a9a8cd
--- /dev/null
+++ b/tests/posix/weird_names.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+bfs_diff '-' '(-' '!-' ',' ')' './(' './!' \( \! -print -o -print \)
diff --git a/tests/posix/xdev.out b/tests/posix/xdev.out
new file mode 100644
index 0000000..6253434
--- /dev/null
+++ b/tests/posix/xdev.out
@@ -0,0 +1,4 @@
+.
+./foo
+./foo/bar
+./mnt
diff --git a/tests/posix/xdev.sh b/tests/posix/xdev.sh
new file mode 100644
index 0000000..c59c5c8
--- /dev/null
+++ b/tests/posix/xdev.sh
@@ -0,0 +1,11 @@
+test "$UNAME" = "Darwin" && skip
+
+cd "$TEST"
+mkdir foo mnt
+
+bfs_sudo mount -t tmpfs tmpfs mnt || skip
+defer bfs_sudo umount mnt
+
+"$XTOUCH" foo/bar mnt/baz
+
+bfs_diff . -xdev
diff --git a/tests/run.sh b/tests/run.sh
new file mode 100644
index 0000000..ad9c0be
--- /dev/null
+++ b/tests/run.sh
@@ -0,0 +1,464 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Running test cases
+
+# Beginning/end of line escape sequences
+BOL=$'\n'
+EOL=$'\n'
+
+# Update $EOL for the terminal size
+update_eol() {
+ # Bash gets $COLUMNS from stderr, so if it's redirected use tput instead
+ local cols="${COLUMNS-}"
+ if [ -z "$cols" ]; then
+ cols=$(tput cols 2>/dev/tty)
+ fi
+
+ # Put the cursor at the last column, then write a space so the next
+ # character will wrap
+ EOL=$'\e['"${cols}G "
+}
+
+# ERR trap for tests
+debug_err() {
+ local ret=$? line func file
+ callers | while read -r line func file; do
+ if [ "$func" = source ]; then
+ debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
+ break
+ fi
+ done
+}
+
+# Source a test
+source_test() (
+ set -eE
+ trap debug_err ERR
+
+ if ((${#MAKE[@]})); then
+ # Close the jobserver pipes
+ exec {READY_PIPE}<&- {DONE_PIPE}>&-
+ fi
+
+ cd "$TMP"
+ source "$@"
+)
+
+# Run a test
+run_test() {
+ if ((VERBOSE_ERRORS)); then
+ source_test "$1"
+ else
+ source_test "$1" 2>"$TMP/$TEST.err"
+ fi
+ ret=$?
+
+ if ((${#MAKE[@]})); then
+ # Write one byte to the done pipe
+ printf . >&$DONE_PIPE
+ fi
+
+ case $ret in
+ 0)
+ if ((VERBOSE_TESTS)); then
+ color printf "${BOL}${GRN}[PASS]${RST} ${BLD}%s${RST}\n" "$TEST"
+ fi
+ ;;
+ $EX_SKIP)
+ if ((VERBOSE_SKIPPED || VERBOSE_TESTS)); then
+ color printf "${BOL}${CYN}[SKIP]${RST} ${BLD}%s${RST}\n" "$TEST"
+ fi
+ ;;
+ *)
+ if ((!VERBOSE_ERRORS)); then
+ cat "$TMP/$TEST.err" >&2
+ fi
+ color printf "${BOL}${RED}[FAIL]${RST} ${BLD}%s${RST}\n" "$TEST"
+ ;;
+ esac
+
+ return $ret
+}
+
+# Count the tests running in the background
+BG=0
+
+# Run a test in the background
+bg_test() {
+ run_test "$1" &
+ ((++BG))
+}
+
+# Reap a finished background test
+reap_test() {
+ ((BG--))
+
+ case "$1" in
+ 0)
+ ((++passed))
+ ;;
+ $EX_SKIP)
+ ((++skipped))
+ ;;
+ *)
+ ((++failed))
+ ;;
+ esac
+}
+
+# Wait for a background test to finish
+wait_test() {
+ local pid
+ wait -n -ppid
+ ret=$?
+ if [ -z "${pid:-}" ]; then
+ debug "${BASH_SOURCE[0]}" $((LINENO - 3)) "${RED}error $ret${RST}" >&$DUPERR
+ exit 1
+ fi
+
+ reap_test $ret
+}
+
+# Wait until we're ready to run another test
+wait_ready() {
+ if ((${#MAKE[@]})); then
+ # We'd like to parse the output of jobs -n, but we can't run it in a
+ # subshell or we won't get the right output
+ jobs -n >"$TMP/jobs"
+ while read -r job status ret foo; do
+ case "$status" in
+ Done)
+ reap_test 0
+ ;;
+ Exit)
+ reap_test $ret
+ ;;
+ esac
+ done <"$TMP/jobs"
+
+ # Read one byte from the ready pipe
+ read -r -N1 -u$READY_PIPE
+ elif ((BG >= JOBS)); then
+ wait_test
+ fi
+}
+
+# Run make as a co-process to use its job control
+comake() {
+ coproc {
+ # We can't just use std{in,out}, due to
+ # https://www.gnu.org/software/make/manual/html_node/Parallel-Input.html
+ exec {DONE_PIPE}<&0 {READY_PIPE}>&1
+ exec "${MAKE[@]}" -s \
+ -f "$TESTS/tests.mk" \
+ DONE=$DONE_PIPE \
+ READY=$READY_PIPE \
+ "${!TEST_CASES[@]}" \
+ </dev/null >/dev/null
+ }
+
+ # coproc pipes aren't inherited by subshells, so dup them
+ exec {READY_PIPE}<&${COPROC[0]} {DONE_PIPE}>&${COPROC[1]}
+}
+
+# Run all the tests
+run_tests() {
+ if ((VERBOSE_TESTS)); then
+ BOL=''
+ elif ((COLOR_STDOUT)); then
+ # Carriage return + clear line
+ BOL=$'\r\e[K'
+
+ # Workaround for bash 4: checkwinsize is off by default. We can turn it
+ # on, but we also have to explicitly trigger a foreground job to finish
+ # so that it will update the window size before we use $COLUMNS
+ shopt -s checkwinsize
+ (:)
+
+ update_eol
+ trap update_eol WINCH
+ fi
+
+ passed=0
+ failed=0
+ skipped=0
+ ran=0
+ total=${#TEST_CASES[@]}
+
+ if ((COLOR_STDOUT || VERBOSE_TESTS)); then
+ TEST_FMT="${BOL}${YLW}[%3d%%]${RST} ${BLD}%s${RST}${EOL}"
+ else
+ TEST_FMT="."
+ fi
+
+ if ((${#MAKE[@]})); then
+ comake
+ fi
+
+ # Turn off set -e (but turn it back on in run_test)
+ set +e
+
+ for TEST in "${TEST_CASES[@]}"; do
+ wait_ready
+ if ((STOP && failed > 0)); then
+ break
+ fi
+
+ percent=$((100 * ran / total))
+ color printf "$TEST_FMT" $percent "$TEST"
+
+ mkdir -p "$TMP/$TEST"
+ OUT="$TMP/$TEST.out"
+
+ bg_test "$TESTS/$TEST.sh"
+ ((++ran))
+ done
+
+ while ((BG > 0)); do
+ wait_test
+ done
+
+ printf "${BOL}"
+
+ if ((passed > 0)); then
+ color printf "${GRN}[PASS]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $passed $total
+ fi
+ if ((skipped > 0)); then
+ color printf "${CYN}[SKIP]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $skipped $total
+ fi
+ if ((failed > 0)); then
+ color printf "${RED}[FAIL]${RST} ${BLD}%3d${RST} / ${BLD}%d${RST}\n" $failed $total
+ exit 1
+ fi
+}
+
+## Utilities for the tests themselves
+
+# Default return value for failed tests
+EX_FAIL=1
+
+# Fail the current test
+fail() {
+ exit $EX_FAIL
+}
+
+# Return value when a test is skipped
+EX_SKIP=77
+
+# Skip the current test
+skip() {
+ if ((VERBOSE_SKIPPED)); then
+ caller | {
+ read -r line file
+ printf "${BOL}"
+ debug "$file" $line "" >&$DUPOUT
+ }
+ fi
+
+ exit $EX_SKIP
+}
+
+# Run a command and check its exit status
+check_exit() {
+ local expected="$1"
+ local actual=0
+ shift
+ "$@" || actual=$?
+ ((actual == expected))
+}
+
+# Run a command with sudo
+bfs_sudo() {
+ if ((${#SUDO[@]})); then
+ "${SUDO[@]}" "$@"
+ else
+ return 1
+ fi
+}
+
+# Get the inode number of a file
+inum() {
+ ls -id "$@" | awk '{ print $1 }'
+}
+
+# Set an ACL on a file
+set_acl() {
+ case "$UNAME" in
+ Darwin)
+ chmod +a "$(id -un) allow read,write" "$1"
+ ;;
+ FreeBSD)
+ if (($(getconf ACL_NFS4 "$1") > 0)); then
+ setfacl -m "u:$(id -un):rw::allow" "$1"
+ else
+ setfacl -m "u:$(id -un):rw" "$1"
+ fi
+ ;;
+ *)
+ setfacl -m "u:$(id -un):rw" "$1"
+ ;;
+ esac
+}
+
+# Print a bfs invocation for --verbose=commands
+bfs_verbose() {
+ if ((VERBOSE_COMMANDS)); then
+ (
+ # Close some fds to make room for the pipe,
+ # even with extremely low ulimit -n
+ exec >&- {DUPERR}>&-
+ exec >&$DUPOUT {DUPOUT}>&-
+ color bfs_verbose_impl "$@"
+ )
+ fi
+}
+
+bfs_verbose_impl() {
+ printf "${GRN}%q${RST}" "${BFS[0]}"
+ if ((${#BFS[@]} > 1)); then
+ printf " ${GRN}%q${RST}" "${BFS[@]:1}"
+ fi
+
+ local expr_started=0 color
+ for arg; do
+ case "$arg" in
+ -[A-Z]*|-[dsxf]|-j*)
+ color="${CYN}"
+ ;;
+ \(|!|-[ao]|-and|-or|-not|-exclude)
+ expr_started=1
+ color="${RED}"
+ ;;
+ \)|,)
+ if ((expr_started)); then
+ color="${RED}"
+ else
+ color="${MAG}"
+ fi
+ ;;
+ -?*)
+ expr_started=1
+ color="${BLU}"
+ ;;
+ *)
+ if ((expr_started)); then
+ color="${BLD}"
+ else
+ color="${MAG}"
+ fi
+ ;;
+ esac
+ printf " ${color}%q${RST}" "$arg"
+ done
+
+ printf '\n'
+}
+
+# Run the bfs we're testing
+invoke_bfs() {
+ bfs_verbose "$@"
+
+ local ret=0
+ # Close the logging fds
+ "${BFS[@]}" "$@" {DUPOUT}>&- {DUPERR}>&- || ret=$?
+
+ # Allow bfs to fail, but not crash
+ if ((ret > 125)); then
+ exit $ret
+ else
+ return $ret
+ fi
+}
+
+if command -v unbuffer &>/dev/null; then
+ UNBUFFER=unbuffer
+elif command -v expect_unbuffer &>/dev/null; then
+ UNBUFFER=expect_unbuffer
+fi
+
+# Run bfs with a pseudo-terminal attached
+bfs_pty() {
+ test -n "${UNBUFFER:-}" || skip
+
+ bfs_verbose "$@"
+
+ local ret=0
+ "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@" </dev/null' bash "${BFS[@]}" "$@" || ret=$?
+
+ if ((ret > 125)); then
+ exit $ret
+ else
+ return $ret
+ fi
+}
+
+# Create a directory tree with xattrs in scratch
+make_xattrs() {
+ cd "$TEST"
+
+ "$XTOUCH" normal xattr xattr_2
+ ln -s xattr link
+ ln -s normal xattr_link
+
+ case "$UNAME" in
+ Darwin)
+ xattr -w bfs_test true xattr \
+ && xattr -w bfs_test_2 true xattr_2 \
+ && xattr -s -w bfs_test true xattr_link
+ ;;
+ FreeBSD)
+ setextattr user bfs_test true xattr \
+ && setextattr user bfs_test_2 true xattr_2 \
+ && setextattr -h user bfs_test true xattr_link
+ ;;
+ *)
+ # Linux tmpfs doesn't support the user.* namespace, so we use the security.*
+ # namespace, which is writable by root and readable by others
+ bfs_sudo setfattr -n security.bfs_test xattr \
+ && bfs_sudo setfattr -n security.bfs_test_2 xattr_2 \
+ && bfs_sudo setfattr -h -n security.bfs_test xattr_link
+ ;;
+ esac
+}
+
+## Snapshot testing
+
+# Return value when a difference is detected
+EX_DIFF=20
+
+# Detect colored diff support
+if diff --color /dev/null /dev/null &>/dev/null; then
+ DIFF="diff --color"
+else
+ DIFF="diff"
+fi
+
+# Sort the output file
+sort_output() {
+ sort -o "$OUT" "$OUT"
+}
+
+# Diff against the expected output
+diff_output() {
+ local GOLD="$TESTS/$TEST.out"
+
+ if ((UPDATE)); then
+ cp "$OUT" "$GOLD"
+ elif ! cmp -s "$GOLD" "$OUT"; then
+ $DIFF -u "$GOLD" "$OUT" >&$DUPERR
+ fi
+}
+
+# Run bfs, and diff it against the expected output
+bfs_diff() {
+ local ret=0
+ invoke_bfs "$@" >"$OUT" || ret=$?
+
+ sort_output
+ diff_output || exit $EX_DIFF
+
+ return $ret
+}
diff --git a/tests/sort-args.sh b/tests/sort-args.sh
index f801d3b..227cc1a 100755
--- a/tests/sort-args.sh
+++ b/tests/sort-args.sh
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
-args=($({ for arg; do echo "$arg"; done } | sort))
+IFS=$'\n' read -rd '' -a args < <(printf '%s\n' "$@" | sort)
echo "${args[@]}"
diff --git a/tests/stddirs.sh b/tests/stddirs.sh
new file mode 100644
index 0000000..e08e6bf
--- /dev/null
+++ b/tests/stddirs.sh
@@ -0,0 +1,152 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Standard directory trees for tests
+
+# Creates a simple file+directory structure for tests
+make_basic() {
+ "$XTOUCH" -p "$1"/{a,b,c/d,e/f,g/h/,i/}
+ "$XTOUCH" -p "$1"/{j/foo,k/foo/bar,l/foo/bar/baz}
+ echo baz >"$1/l/foo/bar/baz"
+}
+
+# Creates a file+directory structure with various permissions for tests
+make_perms() {
+ "$XTOUCH" -p -M000 "$1/0"
+ "$XTOUCH" -p -M444 "$1/r"
+ "$XTOUCH" -p -M222 "$1/w"
+ "$XTOUCH" -p -M644 "$1/rw"
+ "$XTOUCH" -p -M555 "$1/rx"
+ "$XTOUCH" -p -M311 "$1/wx"
+ "$XTOUCH" -p -M755 "$1/rwx"
+}
+
+# Creates a file+directory structure with various symbolic and hard links
+make_links() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln "$1/file" "$1/hardlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ "$XTOUCH" -p "$1/deeply/nested"/{dir/,file}
+ ln -s file "$1/deeply/nested/link"
+ ln -s nowhere "$1/deeply/nested/broken"
+ ln -s deeply/nested "$1/skip"
+}
+
+# Creates a file+directory structure with symbolic link loops
+make_loops() {
+ "$XTOUCH" -p "$1/file"
+ ln -s file "$1/symlink"
+ ln -s nowhere "$1/broken"
+ ln -s symlink/file "$1/notdir"
+ ln -s loop "$1/loop"
+ mkdir -p "$1/deeply/nested/dir"
+ ln -s ../../deeply "$1/deeply/nested/loop"
+ ln -s deeply/nested/loop/nested "$1/skip"
+}
+
+# Creates a file+directory structure with varying timestamps
+make_times() {
+ "$XTOUCH" -p -t "1991-12-14 00:00" "$1/a"
+ "$XTOUCH" -p -t "1991-12-14 00:01" "$1/b"
+ "$XTOUCH" -p -t "1991-12-14 00:02" "$1/c"
+ ln -s a "$1/l"
+ "$XTOUCH" -p -h -t "1991-12-14 00:03" "$1/l"
+ "$XTOUCH" -p -t "1991-12-14 00:04" "$1"
+}
+
+# Creates a file+directory structure with various weird file/directory names
+make_weirdnames() {
+ "$XTOUCH" -p "$1/-/a"
+ "$XTOUCH" -p "$1/(/b"
+ "$XTOUCH" -p "$1/(-/c"
+ "$XTOUCH" -p "$1/!/d"
+ "$XTOUCH" -p "$1/!-/e"
+ "$XTOUCH" -p "$1/,/f"
+ "$XTOUCH" -p "$1/)/g"
+ "$XTOUCH" -p "$1/.../h"
+ "$XTOUCH" -p "$1/\\/i"
+ "$XTOUCH" -p "$1/ /j"
+ "$XTOUCH" -p "$1/[/k"
+}
+
+# Creates a very deep directory structure for testing PATH_MAX handling
+make_deep() {
+ mkdir -p "$1"
+
+ # $name will be 255 characters, aka _XOPEN_NAME_MAX
+ local name="0123456789ABCDEF"
+ name="$name$name$name$name"
+ name="$name$name$name$name"
+ name="${name:0:255}"
+
+ # 4 * 4 * 256 == 4096 >= PATH_MAX
+ local path="$name/$name/$name/$name"
+ path="$path/$path/$path/$path"
+
+ "$XTOUCH" -p "$1"/{{0..9},A,B,C,D,E,F}/"$path/$name"
+}
+
+# Creates a directory structure with many different types, and therefore colors
+make_rainbow() {
+ "$XTOUCH" -p "$1/file.txt"
+ "$XTOUCH" -p "$1/file.dat"
+ "$XTOUCH" -p "$1/lower".{gz,tar,tar.gz}
+ "$XTOUCH" -p "$1/upper".{GZ,TAR,TAR.GZ}
+ "$XTOUCH" -p "$1/lu.tar.GZ" "$1/ul.TAR.gz"
+ ln -s file.txt "$1/link.txt"
+ "$XTOUCH" -p "$1/mh1"
+ ln "$1/mh1" "$1/mh2"
+ mkfifo "$1/pipe"
+ # TODO: block
+ ln -s /dev/null "$1/chardev_link"
+ ln -s nowhere "$1/broken"
+ "$MKSOCK" "$1/socket"
+ "$XTOUCH" -p "$1"/s{u,g,ug}id
+ chmod 06644 "$1"/sugid
+ chmod 04644 "$1"/suid
+ chmod 02644 "$1"/sgid
+ mkdir "$1/ow" "$1"/sticky{,_ow}
+ chmod o+w "$1"/*ow
+ chmod +t "$1"/sticky*
+ "$XTOUCH" -p "$1"/exec.sh
+ chmod +x "$1"/exec.sh
+ "$XTOUCH" -p "$1/"$'\e[1m/\e[0m'
+}
+
+# Create all standard directory trees
+make_stddirs() {
+ TMP=$(mktemp -d "${TMPDIR:-/tmp}"/bfs.XXXXXXXXXX)
+
+ if ((CLEAN)); then
+ defer clean_stddirs
+ else
+ printf "Test files saved to ${BLD}%s${RST}\n" "$TMP"
+ fi
+
+ chown "$(id -u):$(id -g)" "$TMP"
+
+ make_basic "$TMP/basic"
+ make_perms "$TMP/perms"
+ make_links "$TMP/links"
+ make_loops "$TMP/loops"
+ make_times "$TMP/times"
+ make_weirdnames "$TMP/weirdnames"
+ make_deep "$TMP/deep"
+ make_rainbow "$TMP/rainbow"
+}
+
+# Clean up temporary directories on exit
+clean_stddirs() {
+ # Don't force rm to deal with long paths
+ for dir in "$TMP"/deep/*/*; do
+ if [ -d "$dir" ]; then
+ (cd "$dir" && rm -rf *)
+ fi
+ done
+
+ rm -rf "$TMP"
+}
diff --git a/tests/test_L_acl.out b/tests/test_L_acl.out
deleted file mode 100644
index 1dae00a..0000000
--- a/tests/test_L_acl.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/acl
-scratch/link
diff --git a/tests/test_L_capable.out b/tests/test_L_capable.out
deleted file mode 100644
index e5ba3c7..0000000
--- a/tests/test_L_capable.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/capable
-scratch/link
diff --git a/tests/test_L_delete.out b/tests/test_L_delete.out
deleted file mode 100644
index ed0e9a1..0000000
--- a/tests/test_L_delete.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch
-scratch/foo
diff --git a/tests/test_L_mount.out b/tests/test_L_mount.out
deleted file mode 100644
index 2c3c590..0000000
--- a/tests/test_L_mount.out
+++ /dev/null
@@ -1,5 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
-scratch/foo/qux
diff --git a/tests/test_L_xattr.out b/tests/test_L_xattr.out
deleted file mode 100644
index 12fac95..0000000
--- a/tests/test_L_xattr.out
+++ /dev/null
@@ -1,3 +0,0 @@
-scratch/link
-scratch/xattr
-scratch/xattr_2
diff --git a/tests/test_L_xattrname.out b/tests/test_L_xattrname.out
deleted file mode 100644
index 4dc4836..0000000
--- a/tests/test_L_xattrname.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/link
-scratch/xattr
diff --git a/tests/test_L_xdev.out b/tests/test_L_xdev.out
deleted file mode 100644
index 2c3c590..0000000
--- a/tests/test_L_xdev.out
+++ /dev/null
@@ -1,5 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
-scratch/foo/qux
diff --git a/tests/test_Ofast.out b/tests/test_Ofast.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_Ofast.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_S_bfs.out b/tests/test_S_bfs.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_S_bfs.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_S_ids.out b/tests/test_S_ids.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_S_ids.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_acl.out b/tests/test_acl.out
deleted file mode 100644
index ddf8446..0000000
--- a/tests/test_acl.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/acl
diff --git a/tests/test_basic.out b/tests/test_basic.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_basic.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_capable.out b/tests/test_capable.out
deleted file mode 100644
index 78b5bd9..0000000
--- a/tests/test_capable.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/capable
diff --git a/tests/test_closed_stdin.out b/tests/test_closed_stdin.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_closed_stdin.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_color_ls.out b/tests/test_color_ls.out
deleted file mode 100644
index b08d894..0000000
--- a/tests/test_color_ls.out
+++ /dev/null
@@ -1,12 +0,0 @@
-scratch/foo/bar
-scratch/foo/bar
-/__bfs__/nowhere
-/__bfs__/nowhere
-foo/bar/baz/qux
-foo/bar/baz/qux
-foo/bar/nowhere
-foo/bar/nowhere
-foo/bar/nowhere/nothing
-foo/bar/nowhere/nothing
-foo/bar/baz
-foo/bar/baz
diff --git a/tests/test_color_mi.out b/tests/test_color_mi.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_mi.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_nul.out b/tests/test_color_nul.out
deleted file mode 100644
index c328f82..0000000
--- a/tests/test_color_nul.out
+++ /dev/null
Binary files differ
diff --git a/tests/test_color_or0_mi.out b/tests/test_color_or0_mi.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_or0_mi.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_or0_mi0.out b/tests/test_color_or0_mi0.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_or0_mi0.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_st0_tw0_ow0.out b/tests/test_color_st0_tw0_ow0.out
deleted file mode 100644
index 2b86fe4..0000000
--- a/tests/test_color_st0_tw0_ow0.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/ow
-rainbow/sticky
-rainbow/sticky_ow
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sgid
-rainbow/pipe
-rainbow/sugid
-rainbow/suid
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_color_star.out b/tests/test_color_star.out
deleted file mode 100644
index 77fc8a8..0000000
--- a/tests/test_color_star.out
+++ /dev/null
@@ -1,20 +0,0 @@
-rainbow
-rainbow/exec.sh
-rainbow/socket
-rainbow/broken
-rainbow/chardev_link
-rainbow/link.txt
-rainbow/sticky_ow
-rainbow/sgid
-rainbow/pipe
-rainbow/ow
-rainbow/sugid
-rainbow/suid
-rainbow/sticky
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
diff --git a/tests/test_d_path.out b/tests/test_d_path.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_d_path.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_data_flow_group.out b/tests/test_data_flow_group.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_group.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_data_flow_hidden.out b/tests/test_data_flow_hidden.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_hidden.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_data_flow_sparse.out b/tests/test_data_flow_sparse.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_sparse.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_data_flow_user.out b/tests/test_data_flow_user.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_data_flow_user.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_daystart.out b/tests/test_daystart.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_daystart.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_daystart_twice.out b/tests/test_daystart_twice.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_daystart_twice.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_delete.out b/tests/test_delete.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_delete.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_delete_many.out b/tests/test_delete_many.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_delete_many.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_depth.out b/tests/test_depth.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_depth.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_depth_error.out b/tests/test_depth_error.out
deleted file mode 100644
index ed0e9a1..0000000
--- a/tests/test_depth_error.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch
-scratch/foo
diff --git a/tests/test_depth_overflow.out b/tests/test_depth_overflow.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_depth_overflow.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_empty_special.out b/tests/test_empty_special.out
deleted file mode 100644
index 3927f2b..0000000
--- a/tests/test_empty_special.out
+++ /dev/null
@@ -1,14 +0,0 @@
-rainbow/exec.sh
-rainbow/file.dat
-rainbow/file.txt
-rainbow/mh1
-rainbow/mh2
-rainbow/ow
-rainbow/sgid
-rainbow/star.gz
-rainbow/star.tar
-rainbow/star.tar.gz
-rainbow/sticky
-rainbow/sticky_ow
-rainbow/sugid
-rainbow/suid
diff --git a/tests/test_exec.out b/tests/test_exec.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_exec.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_exec_plus_status.out b/tests/test_exec_plus_status.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_exec_plus_status.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_flags.out b/tests/test_flags.out
deleted file mode 100644
index 11998ed..0000000
--- a/tests/test_flags.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/bar
diff --git a/tests/test_fprint_duplicate_stdout.out b/tests/test_fprint_duplicate_stdout.out
deleted file mode 100644
index 6c21751..0000000
--- a/tests/test_fprint_duplicate_stdout.out
+++ /dev/null
@@ -1,38 +0,0 @@
-basic
-basic
-basic/a
-basic/a
-basic/b
-basic/b
-basic/c
-basic/c
-basic/c/d
-basic/c/d
-basic/e
-basic/e
-basic/e/f
-basic/e/f
-basic/g
-basic/g
-basic/g/h
-basic/g/h
-basic/i
-basic/i
-basic/j
-basic/j
-basic/j/foo
-basic/j/foo
-basic/k
-basic/k
-basic/k/foo
-basic/k/foo
-basic/k/foo/bar
-basic/k/foo/bar
-basic/l
-basic/l
-basic/l/foo
-basic/l/foo
-basic/l/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
-basic/l/foo/bar/baz
diff --git a/tests/test_fstype.out b/tests/test_fstype.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_fstype.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_gid.out b/tests/test_gid.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_gid_minus.out b/tests/test_gid_minus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_minus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_gid_minus_plus.out b/tests/test_gid_minus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_minus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_gid_name.out b/tests/test_gid_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_gid_plus.out b/tests/test_gid_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_gid_plus_plus.out b/tests/test_gid_plus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_gid_plus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_group_id.out b/tests/test_group_id.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_group_id.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_group_name.out b/tests/test_group_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_group_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_group_nogroup.out b/tests/test_group_nogroup.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_group_nogroup.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_inum_bind_mount.out b/tests/test_inum_bind_mount.out
deleted file mode 100644
index a520de3..0000000
--- a/tests/test_inum_bind_mount.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/bar
-scratch/foo
diff --git a/tests/test_inum_mount.out b/tests/test_inum_mount.out
deleted file mode 100644
index 99c7511..0000000
--- a/tests/test_inum_mount.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/mnt
diff --git a/tests/test_mount.out b/tests/test_mount.out
deleted file mode 100644
index 005bdcf..0000000
--- a/tests/test_mount.out
+++ /dev/null
@@ -1,4 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
diff --git a/tests/test_path_d.out b/tests/test_path_d.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_path_d.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_printf_Y_error.out b/tests/test_printf_Y_error.out
deleted file mode 100644
index 00d6ee7..0000000
--- a/tests/test_printf_Y_error.out
+++ /dev/null
@@ -1,3 +0,0 @@
-(scratch) () d d
-(scratch/foo) () d d
-(scratch/bar) (foo/bar) l ?
diff --git a/tests/test_printf_nul.out b/tests/test_printf_nul.out
deleted file mode 100644
index 6833fdd..0000000
--- a/tests/test_printf_nul.out
+++ /dev/null
Binary files differ
diff --git a/tests/test_rm.out b/tests/test_rm.out
deleted file mode 100644
index fb188b9..0000000
--- a/tests/test_rm.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch
diff --git a/tests/test_stderr_fails_silently.out b/tests/test_stderr_fails_silently.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_stderr_fails_silently.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_true.out b/tests/test_true.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_true.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_type_bind_mount.out b/tests/test_type_bind_mount.out
deleted file mode 100644
index 6435159..0000000
--- a/tests/test_type_bind_mount.out
+++ /dev/null
@@ -1 +0,0 @@
-scratch/null
diff --git a/tests/test_uid.out b/tests/test_uid.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_uid_minus.out b/tests/test_uid_minus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_minus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_uid_minus_plus.out b/tests/test_uid_minus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_minus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_uid_name.out b/tests/test_uid_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_uid_plus.out b/tests/test_uid_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_uid_plus_plus.out b/tests/test_uid_plus_plus.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_uid_plus_plus.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_unique_depth.out b/tests/test_unique_depth.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_unique_depth.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_user_id.out b/tests/test_user_id.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_user_id.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_user_name.out b/tests/test_user_name.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_user_name.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_user_nouser.out b/tests/test_user_nouser.out
deleted file mode 100644
index bb3cd8d..0000000
--- a/tests/test_user_nouser.out
+++ /dev/null
@@ -1,19 +0,0 @@
-basic
-basic/a
-basic/b
-basic/c
-basic/e
-basic/g
-basic/i
-basic/j
-basic/k
-basic/l
-basic/c/d
-basic/e/f
-basic/g/h
-basic/j/foo
-basic/k/foo
-basic/l/foo
-basic/k/foo/bar
-basic/l/foo/bar
-basic/l/foo/bar/baz
diff --git a/tests/test_xattr.out b/tests/test_xattr.out
deleted file mode 100644
index 109e7c9..0000000
--- a/tests/test_xattr.out
+++ /dev/null
@@ -1,3 +0,0 @@
-scratch/xattr
-scratch/xattr_2
-scratch/xattr_link
diff --git a/tests/test_xattrname.out b/tests/test_xattrname.out
deleted file mode 100644
index 0285ac1..0000000
--- a/tests/test_xattrname.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/xattr
-scratch/xattr_link
diff --git a/tests/test_xdev.out b/tests/test_xdev.out
deleted file mode 100644
index 005bdcf..0000000
--- a/tests/test_xdev.out
+++ /dev/null
@@ -1,4 +0,0 @@
-scratch
-scratch/foo
-scratch/mnt
-scratch/foo/bar
diff --git a/tests/test_xtype_bind_mount.out b/tests/test_xtype_bind_mount.out
deleted file mode 100644
index 16804ea..0000000
--- a/tests/test_xtype_bind_mount.out
+++ /dev/null
@@ -1,2 +0,0 @@
-scratch/link
-scratch/null
diff --git a/tests/tests.h b/tests/tests.h
new file mode 100644
index 0000000..9078938
--- /dev/null
+++ b/tests/tests.h
@@ -0,0 +1,73 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Unit tests.
+ */
+
+#ifndef BFS_TESTS_H
+#define BFS_TESTS_H
+
+#include "prelude.h"
+#include "diag.h"
+
+/** Unit test function type. */
+typedef bool test_fn(void);
+
+/** Memory allocation tests. */
+bool check_alloc(void);
+
+/** Standard library wrapper tests. */
+bool check_bfstd(void);
+
+/** Bit manipulation tests. */
+bool check_bit(void);
+
+/** I/O queue tests. */
+bool check_ioq(void);
+
+/** Trie tests. */
+bool check_trie(void);
+
+/** Process spawning tests. */
+bool check_xspawn(void);
+
+/** Time tests. */
+bool check_xtime(void);
+
+/** Don't ignore the bfs_check() return value. */
+attr(nodiscard)
+static inline bool bfs_check(bool ret) {
+ return ret;
+}
+
+/**
+ * Check a condition, logging a message on failure but continuing.
+ */
+#define bfs_check(...) \
+ bfs_check(bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", ""))
+
+#define bfs_check_(str, cond, format, ...) \
+ ((cond) ? true : (bfs_diag( \
+ sizeof(format) > 1 \
+ ? "%.0s" format "%s%s" \
+ : "Check failed: `%s`%s", \
+ str, __VA_ARGS__), false))
+
+/** Get a string description of the last error. */
+const char *bfs_errstr(void);
+
+/**
+ * Check a condition, logging the current error string on failure.
+ */
+#define bfs_pcheck(...) \
+ bfs_pcheck_(#__VA_ARGS__, __VA_ARGS__, "", "")
+
+#define bfs_pcheck_(str, cond, format, ...) \
+ ((cond) ? true : (bfs_diag( \
+ sizeof(format) > 1 \
+ ? "%.0s" format "%s%s: %s" \
+ : "Check failed: `%s`%s: %s", \
+ str, __VA_ARGS__, bfs_errstr()), false))
+
+#endif // BFS_TESTS_H
diff --git a/tests/tests.mk b/tests/tests.mk
new file mode 100644
index 0000000..035ca79
--- /dev/null
+++ b/tests/tests.mk
@@ -0,0 +1,13 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that exposes make's job control to tests.sh
+
+# BSD make will chdir into ${.OBJDIR} by default, unless we tell it not to
+.OBJDIR: .
+
+# Turn off implicit rules
+.SUFFIXES:
+
+.DEFAULT::
+ bash -c 'printf . >&$(READY) && read -r -N1 -u$(DONE)'
diff --git a/tests/tests.sh b/tests/tests.sh
new file mode 100755
index 0000000..3890243
--- /dev/null
+++ b/tests/tests.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+set -euP
+umask 022
+
+TESTS="$(dirname -- "${BASH_SOURCE[0]}")"
+. "$TESTS/util.sh"
+. "$TESTS/color.sh"
+. "$TESTS/stddirs.sh"
+. "$TESTS/getopts.sh"
+. "$TESTS/run.sh"
+
+stdenv
+drop_root "$@"
+parse_args "$@"
+make_stddirs
+run_tests
diff --git a/tests/trie.c b/tests/trie.c
index 4326196..4667322 100644
--- a/tests/trie.c
+++ b/tests/trie.c
@@ -1,23 +1,10 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020-2022 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#undef NDEBUG
-
-#include "../trie.h"
-#include <assert.h>
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "trie.h"
+#include "diag.h"
#include <stdlib.h>
#include <string.h>
@@ -50,14 +37,16 @@ const char *keys[] = {
">>>",
};
-const size_t nkeys = sizeof(keys) / sizeof(keys[0]);
+const size_t nkeys = countof(keys);
+
+bool check_trie(void) {
+ bool ret = true;
-int main(void) {
struct trie trie;
trie_init(&trie);
for (size_t i = 0; i < nkeys; ++i) {
- assert(!trie_find_str(&trie, keys[i]));
+ ret &= bfs_check(!trie_find_str(&trie, keys[i]));
const char *prefix = NULL;
for (size_t j = 0; j < i; ++j) {
@@ -70,27 +59,38 @@ int main(void) {
struct trie_leaf *leaf = trie_find_prefix(&trie, keys[i]);
if (prefix) {
- assert(leaf);
- assert(strcmp(prefix, leaf->key) == 0);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(prefix, leaf->key) == 0);
} else {
- assert(!leaf);
+ ret &= bfs_check(!leaf);
}
leaf = trie_insert_str(&trie, keys[i]);
- assert(leaf);
- assert(strcmp(keys[i], leaf->key) == 0);
- assert(leaf->length == strlen(keys[i]) + 1);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(keys[i], leaf->key) == 0);
+ ret &= bfs_check(leaf->length == strlen(keys[i]) + 1);
+ }
+
+ {
+ size_t i = 0;
+ for_trie (leaf, &trie) {
+ ret &= bfs_check(leaf == trie_find_str(&trie, keys[i]));
+ ret &= bfs_check(!leaf->prev || leaf->prev->next == leaf);
+ ret &= bfs_check(!leaf->next || leaf->next->prev == leaf);
+ ++i;
+ }
+ ret &= bfs_check(i == nkeys);
}
for (size_t i = 0; i < nkeys; ++i) {
struct trie_leaf *leaf = trie_find_str(&trie, keys[i]);
- assert(leaf);
- assert(strcmp(keys[i], leaf->key) == 0);
- assert(leaf->length == strlen(keys[i]) + 1);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(keys[i], leaf->key) == 0);
+ ret &= bfs_check(leaf->length == strlen(keys[i]) + 1);
trie_remove(&trie, leaf);
leaf = trie_find_str(&trie, keys[i]);
- assert(!leaf);
+ ret &= bfs_check(!leaf);
const char *postfix = NULL;
for (size_t j = i + 1; j < nkeys; ++j) {
@@ -103,31 +103,35 @@ int main(void) {
leaf = trie_find_postfix(&trie, keys[i]);
if (postfix) {
- assert(leaf);
- assert(strcmp(postfix, leaf->key) == 0);
+ bfs_verify(leaf);
+ ret &= bfs_check(strcmp(postfix, leaf->key) == 0);
} else {
- assert(!leaf);
+ ret &= bfs_check(!leaf);
}
}
+ for_trie (leaf, &trie) {
+ ret &= bfs_check(false, "trie should be empty");
+ }
+
// This tests the "jump" node handling on 32-bit platforms
size_t longsize = 1 << 20;
char *longstr = malloc(longsize);
- assert(longstr);
+ bfs_verify(longstr);
memset(longstr, 0xAC, longsize);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize));
- memset(longstr + longsize/2, 0xAB, longsize/2);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ memset(longstr + longsize / 2, 0xAB, longsize / 2);
+ ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize));
- memset(longstr, 0xAA, longsize/2);
- assert(!trie_find_mem(&trie, longstr, longsize));
- assert(trie_insert_mem(&trie, longstr, longsize));
+ memset(longstr, 0xAA, longsize / 2);
+ ret &= bfs_check(!trie_find_mem(&trie, longstr, longsize));
+ ret &= bfs_check(trie_insert_mem(&trie, longstr, longsize));
free(longstr);
trie_destroy(&trie);
- return EXIT_SUCCESS;
+ return ret;
}
diff --git a/tests/util.sh b/tests/util.sh
new file mode 100644
index 0000000..7dba9fb
--- /dev/null
+++ b/tests/util.sh
@@ -0,0 +1,185 @@
+#!/hint/bash
+
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+## Utility functions
+
+# Portable realpath(1)
+_realpath() (
+ cd "$(dirname -- "$1")"
+ echo "$PWD/$(basename -- "$1")"
+)
+
+# Globals
+TESTS=$(_realpath "$TESTS")
+if [ "${BUILDDIR-}" ]; then
+ BIN=$(_realpath "$BUILDDIR/bin")
+else
+ BIN=$(_realpath "$TESTS/../bin")
+fi
+MKSOCK="$BIN/tests/mksock"
+XTOUCH="$BIN/tests/xtouch"
+UNAME=$(uname)
+
+# Standardize the environment
+stdenv() {
+ export LC_ALL=C
+ export TZ=UTC0
+
+ local SAN_OPTIONS="abort_on_error=1:halt_on_error=1:log_to_syslog=0"
+ export ASAN_OPTIONS="$SAN_OPTIONS"
+ export LSAN_OPTIONS="$SAN_OPTIONS"
+ export MSAN_OPTIONS="$SAN_OPTIONS"
+ export TSAN_OPTIONS="$SAN_OPTIONS"
+ export UBSAN_OPTIONS="$SAN_OPTIONS"
+
+ export LS_COLORS=""
+ unset BFS_COLORS
+
+ if [ "$UNAME" = Darwin ]; then
+ # ASan on macOS likes to report
+ #
+ # malloc: nano zone abandoned due to inability to preallocate reserved vm space.
+ #
+ # to syslog, which as a side effect opens a socket which might take the
+ # place of one of the standard streams if the process is launched with
+ # it closed. This environment variable avoids the message.
+ export MallocNanoZone=0
+ fi
+
+ # Count the inherited FDs
+ if [ -d /proc/self/fd ]; then
+ local fds=/proc/self/fd
+ else
+ local fds=/dev/fd
+ fi
+ # We use ls $fds on purpose, rather than e.g. ($fds/*), to avoid counting
+ # internal bash fds that are not exposed to spawned processes
+ NOPENFD=$(ls -1q "$fds/" 2>/dev/null | wc -l)
+ NOPENFD=$((NOPENFD > 3 ? NOPENFD - 1 : 3))
+
+ # Close stdin so bfs doesn't think we're interactive
+ # dup() the standard fds for logging even when redirected
+ exec </dev/null {DUPOUT}>&1 {DUPERR}>&2
+}
+
+# Drop root priviliges or bail
+drop_root() {
+ if command -v capsh &>/dev/null; then
+ if capsh --has-p=cap_dac_override &>/dev/null || capsh --has-p=cap_dac_read_search &>/dev/null; then
+ if [ -n "${BFS_TRIED_DROP:-}" ]; then
+ color cat >&2 <<EOF
+${RED}error:${RST} Failed to drop capabilities.
+EOF
+
+ exit 1
+ fi
+
+ color cat >&2 <<EOF
+${YLW}warning:${RST} Running as ${BLD}$(id -un)${RST} is not recommended. Dropping ${BLD}cap_dac_override${RST} and
+${BLD}cap_dac_read_search${RST}.
+
+EOF
+
+ BFS_TRIED_DROP=y exec capsh \
+ --drop=cap_dac_override,cap_dac_read_search \
+ --caps=cap_dac_override,cap_dac_read_search-eip \
+ -- "$0" "$@"
+ fi
+ elif ((EUID == 0)); then
+ UNLESS=
+ if [ "$UNAME" = "Linux" ]; then
+ UNLESS=" unless ${GRN}capsh${RST} is installed"
+ fi
+
+ color cat >&2 <<EOF
+${RED}error:${RST} These tests expect filesystem permissions to be enforced, and therefore
+will not work when run as ${BLD}$(id -un)${RST}${UNLESS}.
+EOF
+ exit 1
+ fi
+}
+
+## Debugging
+
+# Get the bash call stack
+callers() {
+ local frame=0
+ while caller $frame; do
+ ((++frame))
+ done
+}
+
+# Print a message including path, line number, and command
+debug() {
+ local file="$1"
+ local line="$2"
+ local msg="$3"
+ local cmd="$(awk "NR == $line" "$file" 2>/dev/null)" || :
+ file="${file/#*\/tests\//tests/}"
+
+ color printf "${BLD}%s:%d:${RST} %s\n %s\n" "$file" "$line" "$msg" "$cmd"
+}
+
+## Deferred cleanup
+
+# Quote a command safely for eval
+quote() {
+ printf '%q' "$1"
+ shift
+ if (($# > 0)); then
+ printf ' %q' "$@"
+ fi
+}
+
+DEFER_LEVEL=-1
+
+# Run a command when this (sub)shell exits
+defer() {
+ # Check if the EXIT trap is already set
+ if ((DEFER_LEVEL != BASH_SUBSHELL)); then
+ DEFER_LEVEL=$BASH_SUBSHELL
+ DEFER_CMDS=()
+ DEFER_LINES=()
+ DEFER_FILES=()
+ trap pop_defers EXIT
+ fi
+
+ DEFER_CMDS+=("$(quote "$@")")
+
+ local line file
+ read -r line file < <(caller)
+ DEFER_LINES+=("$line")
+ DEFER_FILES+=("$file")
+}
+
+# Pop a single command from the defer stack and run it
+pop_defer() {
+ local cmd="${DEFER_CMDS[-1]}"
+ local file="${DEFER_FILES[-1]}"
+ local line="${DEFER_LINES[-1]}"
+ unset "DEFER_CMDS[-1]"
+ unset "DEFER_FILES[-1]"
+ unset "DEFER_LINES[-1]"
+
+ local ret=0
+ eval "$cmd" || ret=$?
+
+ if ((ret != 0)); then
+ debug "$file" $line "${RED}error $ret${RST}" >&$DUPERR
+ fi
+
+ return $ret
+}
+
+# Run all deferred commands
+pop_defers() {
+ local ret=0
+
+ while ((${#DEFER_CMDS[@]} > 0)); do
+ pop_defer || ret=$?
+ done
+
+ return $ret
+}
diff --git a/tests/xspawn.c b/tests/xspawn.c
new file mode 100644
index 0000000..7362aa5
--- /dev/null
+++ b/tests/xspawn.c
@@ -0,0 +1,206 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "alloc.h"
+#include "bfstd.h"
+#include "dstring.h"
+#include "xspawn.h"
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+
+/** Duplicate the current environment. */
+static char **envdup(void) {
+ extern char **environ;
+
+ char **envp = NULL;
+ size_t envc = 0;
+
+ for (char **var = environ; ; ++var) {
+ char *copy = NULL;
+ if (*var) {
+ copy = strdup(*var);
+ if (!copy) {
+ goto fail;
+ }
+ }
+
+ char **dest = RESERVE(char *, &envp, &envc);
+ if (!dest) {
+ free(copy);
+ goto fail;
+ }
+ *dest = copy;
+
+ if (!*var) {
+ break;
+ }
+ }
+
+ return envp;
+
+fail:
+ for (size_t i = 0; i < envc; ++i) {
+ free(envp[i]);
+ }
+ free(envp);
+ return NULL;
+}
+
+/** Check that we resolve executables in $PATH correctly. */
+static bool check_use_path(bool use_posix) {
+ bool ret = true;
+
+ struct bfs_spawn spawn;
+ ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0);
+ if (!ret) {
+ goto out;
+ }
+
+ spawn.flags |= BFS_SPAWN_USE_PATH;
+ if (!use_posix) {
+ spawn.flags &= ~BFS_SPAWN_USE_POSIX;
+ }
+
+ const char *builddir = getenv("BUILDDIR");
+ dchar *bin = dstrprintf("%s/bin", builddir ? builddir : ".");
+ ret &= bfs_pcheck(bin, "dstrprintf()");
+ if (!ret) {
+ goto destroy;
+ }
+
+ ret &= bfs_pcheck(bfs_spawn_addopen(&spawn, 10, bin, O_RDONLY | O_DIRECTORY, 0) == 0);
+ ret &= bfs_pcheck(bfs_spawn_adddup2(&spawn, 10, 11) == 0);
+ ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 10) == 0);
+ ret &= bfs_pcheck(bfs_spawn_addfchdir(&spawn, 11) == 0);
+ ret &= bfs_pcheck(bfs_spawn_addclose(&spawn, 11) == 0);
+ if (!ret) {
+ goto bin;
+ }
+
+ // Check that $PATH is resolved in the parent's environment
+ char **envp;
+ ret &= bfs_pcheck(envp = envdup());
+ if (!ret) {
+ goto bin;
+ }
+
+ // Check that $PATH is resolved after the file actions
+ char *old_path = getenv("PATH");
+ dchar *new_path = NULL;
+ if (old_path) {
+ ret &= bfs_pcheck(old_path = strdup(old_path));
+ if (!ret) {
+ goto env;
+ }
+ new_path = dstrprintf("tests:%s", old_path);
+ } else {
+ new_path = dstrdup("tests");
+ }
+ ret &= bfs_check(new_path);
+ if (!ret) {
+ goto path;
+ }
+
+ ret &= bfs_pcheck(setenv("PATH", new_path, true) == 0);
+ if (!ret) {
+ goto path;
+ }
+
+ char *argv[] = {"xspawnee", old_path, NULL};
+ pid_t pid = bfs_spawn("xspawnee", &spawn, argv, envp);
+ ret &= bfs_pcheck(pid >= 0, "bfs_spawn()");
+ if (!ret) {
+ goto unset;
+ }
+
+ int wstatus;
+ ret &= bfs_pcheck(xwaitpid(pid, &wstatus, 0) == pid)
+ && bfs_check(WIFEXITED(wstatus));
+ if (ret) {
+ int wexit = WEXITSTATUS(wstatus);
+ ret &= bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit);
+ }
+
+unset:
+ if (old_path) {
+ ret &= bfs_pcheck(setenv("PATH", old_path, true) == 0);
+ } else {
+ ret &= bfs_pcheck(unsetenv("PATH") == 0);
+ }
+path:
+ dstrfree(new_path);
+ free(old_path);
+env:
+ for (char **var = envp; *var; ++var) {
+ free(*var);
+ }
+ free(envp);
+bin:
+ dstrfree(bin);
+destroy:
+ ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0);
+out:
+ return ret;
+}
+
+/** Check path resolution of non-existent executables. */
+static bool check_enoent(bool use_posix) {
+ bool ret = true;
+
+ struct bfs_spawn spawn;
+ ret &= bfs_pcheck(bfs_spawn_init(&spawn) == 0);
+ if (!ret) {
+ goto out;
+ }
+
+ spawn.flags |= BFS_SPAWN_USE_PATH;
+ if (!use_posix) {
+ spawn.flags &= ~BFS_SPAWN_USE_POSIX;
+ }
+
+ char *argv[] = {"eW6f5RM9Qi", NULL};
+ pid_t pid = bfs_spawn("eW6f5RM9Qi", &spawn, argv, NULL);
+ ret &= bfs_pcheck(pid < 0 && errno == ENOENT, "bfs_spawn()");
+
+ ret &= bfs_pcheck(bfs_spawn_destroy(&spawn) == 0);
+out:
+ return ret;
+}
+
+static bool check_resolve(void) {
+ bool ret = true;
+ char *exe;
+
+ exe = bfs_spawn_resolve("sh");
+ ret &= bfs_pcheck(exe, "bfs_spawn_resolve('sh')");
+ free(exe);
+
+ exe = bfs_spawn_resolve("/bin/sh");
+ ret &= bfs_pcheck(exe && strcmp(exe, "/bin/sh") == 0);
+ free(exe);
+
+ exe = bfs_spawn_resolve("bin/tests/xspawnee");
+ ret &= bfs_pcheck(exe && strcmp(exe, "bin/tests/xspawnee") == 0);
+ free(exe);
+
+ ret &= bfs_pcheck(!bfs_spawn_resolve("eW6f5RM9Qi") && errno == ENOENT);
+
+ return ret;
+}
+
+bool check_xspawn(void) {
+ bool ret = true;
+
+ ret &= check_use_path(true);
+ ret &= check_use_path(false);
+
+ ret &= check_enoent(true);
+ ret &= check_enoent(false);
+
+ ret &= check_resolve();
+
+ return ret;
+}
diff --git a/tests/xspawnee.c b/tests/xspawnee.c
new file mode 100644
index 0000000..b0a76ca
--- /dev/null
+++ b/tests/xspawnee.c
@@ -0,0 +1,17 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <stdlib.h>
+#include <string.h>
+
+/** Child binary for bfs_spawn() tests. */
+int main(int argc, char *argv[]) {
+ if (argc >= 2) {
+ const char *path = getenv("PATH");
+ if (!path || strcmp(path, argv[1]) != 0) {
+ return EXIT_FAILURE;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/xtime.c b/tests/xtime.c
new file mode 100644
index 0000000..a7c63d2
--- /dev/null
+++ b/tests/xtime.c
@@ -0,0 +1,191 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "tests.h"
+#include "xtime.h"
+#include "bfstd.h"
+#include "diag.h"
+#include <errno.h>
+#include <limits.h>
+#include <stdint.h>
+#include <time.h>
+
+static bool tm_equal(const struct tm *tma, const struct tm *tmb) {
+ return tma->tm_year == tmb->tm_year
+ && tma->tm_mon == tmb->tm_mon
+ && tma->tm_mday == tmb->tm_mday
+ && tma->tm_hour == tmb->tm_hour
+ && tma->tm_min == tmb->tm_min
+ && tma->tm_sec == tmb->tm_sec
+ && tma->tm_wday == tmb->tm_wday
+ && tma->tm_yday == tmb->tm_yday
+ && tma->tm_isdst == tmb->tm_isdst;
+}
+
+/** Check one xgetdate() result. */
+static bool check_one_xgetdate(const char *str, int error, time_t expected) {
+ struct timespec ts;
+ int ret = xgetdate(str, &ts);
+
+ if (error) {
+ return bfs_pcheck(ret == -1 && errno == error, "xgetdate('%s')", str);
+ } else {
+ return bfs_pcheck(ret == 0, "xgetdate('%s')", str)
+ && bfs_check(ts.tv_sec == expected && ts.tv_nsec == 0,
+ "xgetdate('%s'): %jd.%09jd != %jd",
+ str, (intmax_t)ts.tv_sec, (intmax_t)ts.tv_nsec, (intmax_t)expected);
+ }
+}
+
+/** xgetdate() tests. */
+static bool check_xgetdate(void) {
+ bool ret = true;
+
+ ret &= check_one_xgetdate("", EINVAL, 0);
+ ret &= check_one_xgetdate("????", EINVAL, 0);
+ ret &= check_one_xgetdate("1991", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14", 0, 692668800);
+ ret &= check_one_xgetdate("1991-12-14-", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T10", 0, 692704800);
+ ret &= check_one_xgetdate("1991-12-14T10:??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T10:11", 0, 692705460);
+ ret &= check_one_xgetdate("1991-12-14T10:11:??", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T10:11:12", 0, 692705472);
+ ret &= check_one_xgetdate("1991-12-14T10Z", 0, 692704800);
+ ret &= check_one_xgetdate("1991-12-14T10:11Z", 0, 692705460);
+ ret &= check_one_xgetdate("1991-12-14T10:11:12Z", 0, 692705472);
+ ret &= check_one_xgetdate("1991-12-14T10:11:12?", EINVAL, 0);
+ ret &= check_one_xgetdate("1991-12-14T03-07", 0, 692704800);
+ ret &= check_one_xgetdate("1991-12-14T06:41-03:30", 0, 692705460);
+ ret &= check_one_xgetdate("1991-12-14T03:11:12-07:00", 0, 692705472);
+ ret &= check_one_xgetdate("19911214 031112-0700", 0, 692705472);;
+
+ return ret;
+}
+
+#define TM_FORMAT "%04d-%02d-%02d %02d:%02d:%02d (%d/7, %d/365%s)"
+
+#define TM_PRINTF(tm) \
+ (1900 + (tm).tm_year), (tm).tm_mon, (tm).tm_mday, \
+ (tm).tm_hour, (tm).tm_min, (tm).tm_sec, \
+ ((tm).tm_wday + 1), ((tm).tm_yday + 1), \
+ ((tm).tm_isdst ? ((tm).tm_isdst < 0 ? ", DST?" : ", DST") : "")
+
+/** Check one xmktime() result. */
+static bool check_one_xmktime(time_t expected) {
+ struct tm tm;
+ if (!localtime_r(&expected, &tm)) {
+ bfs_diag("localtime_r(%jd): %s", (intmax_t)expected, xstrerror(errno));
+ return false;
+ }
+
+ time_t actual;
+ return bfs_pcheck(xmktime(&tm, &actual) == 0, "xmktime(" TM_FORMAT ")", TM_PRINTF(tm))
+ && bfs_check(actual == expected, "xmktime(" TM_FORMAT "): %jd != %jd", TM_PRINTF(tm), (intmax_t)actual, (intmax_t)expected);
+}
+
+/** xmktime() tests. */
+static bool check_xmktime(void) {
+ bool ret = true;
+
+ for (time_t time = -10; time <= 10; ++time) {
+ ret &= check_one_xmktime(time);
+ }
+
+ // Attempt to trigger overflow (but don't test for it, since it's not mandatory)
+ struct tm tm = {
+ .tm_year = INT_MAX,
+ .tm_mon = INT_MAX,
+ .tm_mday = INT_MAX,
+ .tm_hour = INT_MAX,
+ .tm_min = INT_MAX,
+ .tm_sec = INT_MAX,
+ .tm_isdst = -1,
+ };
+ time_t time;
+ xmktime(&tm, &time);
+
+ return ret;
+}
+
+/** Check one xtimegm() result. */
+static bool check_one_xtimegm(const struct tm *tm) {
+ struct tm tma = *tm, tmb = *tm;
+ time_t ta, tb;
+ ta = mktime(&tma);
+ if (xtimegm(&tmb, &tb) != 0) {
+ tb = -1;
+ }
+
+ bool ret = true;
+ ret &= bfs_check(ta == tb, "%jd != %jd", (intmax_t)ta, (intmax_t)tb);
+ ret &= bfs_check(ta == -1 || tm_equal(&tma, &tmb));
+
+ if (!ret) {
+ bfs_diag("mktime(): " TM_FORMAT, TM_PRINTF(tma));
+ bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(tmb));
+ bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm));
+ }
+
+ return ret;
+}
+
+/** Check an overflowing xtimegm() call. */
+static bool check_xtimegm_overflow(const struct tm *tm) {
+ struct tm copy = *tm;
+ time_t time = 123;
+
+ bool ret = true;
+ ret &= bfs_check(xtimegm(&copy, &time) == -1 && errno == EOVERFLOW);
+ ret &= bfs_check(tm_equal(&copy, tm));
+ ret &= bfs_check(time == 123);
+
+ if (!ret) {
+ bfs_diag("xtimegm(): " TM_FORMAT, TM_PRINTF(copy));
+ bfs_diag("(input): " TM_FORMAT, TM_PRINTF(*tm));
+ }
+
+ return ret;
+}
+
+/** xtimegm() tests. */
+static bool check_xtimegm(void) {
+ bool ret = true;
+
+ struct tm tm = {
+ .tm_isdst = -1,
+ };
+
+ // Check equivalence with mktime()
+ for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10)
+ for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3)
+ for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4)
+ for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5)
+ for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31)
+ for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) {
+ ret &= check_one_xtimegm(&tm);
+ }
+
+ // Check integer overflow cases
+ check_xtimegm_overflow(&(struct tm) { .tm_sec = INT_MAX, .tm_min = INT_MAX });
+ check_xtimegm_overflow(&(struct tm) { .tm_min = INT_MAX, .tm_hour = INT_MAX });
+ check_xtimegm_overflow(&(struct tm) { .tm_hour = INT_MAX, .tm_mday = INT_MAX });
+ check_xtimegm_overflow(&(struct tm) { .tm_mon = INT_MAX, .tm_year = INT_MAX });
+
+ return ret;
+}
+
+bool check_xtime(void) {
+ bool ret = true;
+ ret &= check_xgetdate();
+ ret &= check_xmktime();
+ ret &= check_xtimegm();
+ return ret;
+}
diff --git a/tests/xtimegm.c b/tests/xtimegm.c
deleted file mode 100644
index 2788dee..0000000
--- a/tests/xtimegm.c
+++ /dev/null
@@ -1,107 +0,0 @@
-/****************************************************************************
- * bfs *
- * Copyright (C) 2020 Tavian Barnes <tavianator@tavianator.com> *
- * *
- * Permission to use, copy, modify, and/or distribute this software for any *
- * purpose with or without fee is hereby granted. *
- * *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF *
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR *
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES *
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN *
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF *
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *
- ****************************************************************************/
-
-#include "../time.h"
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <time.h>
-
-static bool tm_equal(const struct tm *tma, const struct tm *tmb) {
- if (tma->tm_year != tmb->tm_year) {
- return false;
- }
- if (tma->tm_mon != tmb->tm_mon) {
- return false;
- }
- if (tma->tm_mday != tmb->tm_mday) {
- return false;
- }
- if (tma->tm_hour != tmb->tm_hour) {
- return false;
- }
- if (tma->tm_min != tmb->tm_min) {
- return false;
- }
- if (tma->tm_sec != tmb->tm_sec) {
- return false;
- }
- if (tma->tm_wday != tmb->tm_wday) {
- return false;
- }
- if (tma->tm_yday != tmb->tm_yday) {
- return false;
- }
- if (tma->tm_isdst != tmb->tm_isdst) {
- return false;
- }
-
- return true;
-}
-
-static void tm_print(FILE *file, const struct tm *tm) {
- fprintf(file, "Y%d M%d D%d h%d m%d s%d wd%d yd%d%s\n",
- tm->tm_year, tm->tm_mon, tm->tm_mday,
- tm->tm_hour, tm->tm_min, tm->tm_sec,
- tm->tm_wday, tm->tm_yday,
- tm->tm_isdst ? (tm->tm_isdst < 0 ? " (DST?)" : " (DST)") : "");
-}
-
-int main(void) {
- if (setenv("TZ", "UTC0", true) != 0) {
- perror("setenv()");
- return EXIT_FAILURE;
- }
-
- struct tm tm = {
- .tm_isdst = -1,
- };
-
- for (tm.tm_year = 10; tm.tm_year <= 200; tm.tm_year += 10)
- for (tm.tm_mon = -3; tm.tm_mon <= 15; tm.tm_mon += 3)
- for (tm.tm_mday = -31; tm.tm_mday <= 61; tm.tm_mday += 4)
- for (tm.tm_hour = -1; tm.tm_hour <= 24; tm.tm_hour += 5)
- for (tm.tm_min = -1; tm.tm_min <= 60; tm.tm_min += 31)
- for (tm.tm_sec = -60; tm.tm_sec <= 120; tm.tm_sec += 5) {
- struct tm tma = tm, tmb = tm;
- time_t ta, tb;
- ta = mktime(&tma);
- if (xtimegm(&tmb, &tb) != 0) {
- tb = -1;
- }
-
- bool fail = false;
- if (ta != tb) {
- printf("Mismatch: %jd != %jd\n", (intmax_t)ta, (intmax_t)tb);
- fail = true;
- }
- if (ta != -1 && !tm_equal(&tma, &tmb)) {
- printf("mktime(): ");
- tm_print(stdout, &tma);
- printf("xtimegm(): ");
- tm_print(stdout, &tmb);
- fail = true;
- }
- if (fail) {
- printf("Input: ");
- tm_print(stdout, &tm);
- return EXIT_FAILURE;
- }
- }
-
- return EXIT_SUCCESS;
-}
diff --git a/tests/xtouch.c b/tests/xtouch.c
new file mode 100644
index 0000000..cd41842
--- /dev/null
+++ b/tests/xtouch.c
@@ -0,0 +1,282 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include "prelude.h"
+#include "bfstd.h"
+#include "sanity.h"
+#include "xtime.h"
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+/** Parsed xtouch arguments. */
+struct args {
+ /** Simple flags. */
+ enum {
+ /** Don't create nonexistent files (-c). */
+ NO_CREATE = 1 << 0,
+ /** Don't follow symlinks (-h). */
+ NO_FOLLOW = 1 << 1,
+ /** Create any missing parent directories (-p). */
+ CREATE_PARENTS = 1 << 2,
+ } flags;
+
+ /** Timestamps (-r|-t|-d). */
+ struct timespec times[2];
+
+ /** File creation mode (-M; default 0666 & ~umask). */
+ mode_t fmode;
+ /** Directory creation mode (-M; default 0777 & ~umask). */
+ mode_t dmode;
+ /** Parent directory creation mode (0777 & ~umask). */
+ mode_t pmode;
+};
+
+/** Open (and maybe create) a single directory. */
+static int open_dir(const struct args *args, int dfd, const char *path) {
+ int ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);
+
+ if (ret < 0 && errno == ENOENT && (args->flags & CREATE_PARENTS)) {
+ if (mkdirat(dfd, path, args->pmode) == 0 || errno == EEXIST) {
+ ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);
+ }
+ }
+
+ return ret;
+}
+
+/** Open (and maybe create) the parent directory of the path. */
+static int open_parent(const struct args *args, const char **path) {
+ size_t max = xbaseoff(*path);
+ if (max == 0) {
+ return AT_FDCWD;
+ }
+
+ char *dir = strndup(*path, max);
+ if (!dir) {
+ return -1;
+ }
+
+ // Optimistically try the whole path first
+ int dfd = open_dir(args, AT_FDCWD, dir);
+ if (dfd >= 0) {
+ goto done;
+ }
+
+ if (errno == ENOENT) {
+ if (!(args->flags & CREATE_PARENTS)) {
+ goto err;
+ }
+ } else if (!errno_is_like(ENAMETOOLONG)) {
+ goto err;
+ }
+
+ // Open the parents one-at-a-time
+ dfd = AT_FDCWD;
+ char *cur = dir;
+ while (*cur) {
+ char *next = cur;
+ next += strcspn(next, "/");
+ next += strspn(next, "/");
+
+ char c = *next;
+ *next = '\0';
+
+ int parent = dfd;
+ dfd = open_dir(args, parent, cur);
+ if (parent >= 0) {
+ close_quietly(parent);
+ }
+ if (dfd < 0) {
+ goto err;
+ }
+
+ *next = c;
+ cur = next;
+ }
+
+done:
+ *path += max;
+err:
+ free(dir);
+ return dfd;
+}
+
+/** Compute flags for fstatat()/utimensat(). */
+static int at_flags(const struct args *args) {
+ if (args->flags & NO_FOLLOW) {
+ return AT_SYMLINK_NOFOLLOW;
+ } else {
+ return 0;
+ }
+}
+
+/** Touch one path. */
+static int xtouch(const struct args *args, const char *path) {
+ int dfd = open_parent(args, &path);
+ if (dfd < 0 && dfd != AT_FDCWD) {
+ return -1;
+ }
+
+ int ret = utimensat(dfd, path, args->times, at_flags(args));
+ if (ret == 0 || errno != ENOENT) {
+ goto done;
+ }
+
+ if (args->flags & NO_CREATE) {
+ ret = 0;
+ goto done;
+ }
+
+ size_t len = strlen(path);
+ if (len > 0 && path[len - 1] == '/') {
+ if (mkdirat(dfd, path, args->dmode) == 0) {
+ ret = utimensat(dfd, path, args->times, at_flags(args));
+ }
+ } else {
+ int fd = openat(dfd, path, O_WRONLY | O_CREAT, args->fmode);
+ if (fd >= 0) {
+ if (futimens(fd, args->times) == 0) {
+ ret = xclose(fd);
+ } else {
+ close_quietly(fd);
+ }
+ }
+ }
+
+done:
+ if (dfd >= 0) {
+ close_quietly(dfd);
+ }
+ return ret;
+}
+
+int main(int argc, char *argv[]) {
+ tzset();
+
+ mode_t mask = umask(0);
+
+ struct args args = {
+ .flags = 0,
+ .times = {
+ { .tv_nsec = UTIME_OMIT },
+ { .tv_nsec = UTIME_OMIT },
+ },
+ .fmode = 0666 & ~mask,
+ .dmode = 0777 & ~mask,
+ .pmode = 0777 & ~mask,
+ };
+
+ bool atime = false, mtime = false;
+ const char *darg = NULL;
+ const char *marg = NULL;
+ const char *rarg = NULL;
+
+ const char *cmd = argc > 0 ? argv[0] : "xtouch";
+ int c;
+ while (c = getopt(argc, argv, ":M:acd:hmpr:t:"), c != -1) {
+ switch (c) {
+ case 'M':
+ marg = optarg;
+ break;
+ case 'a':
+ atime = true;
+ break;
+ case 'c':
+ args.flags |= NO_CREATE;
+ break;
+ case 'd':
+ case 't':
+ darg = optarg;
+ break;
+ case 'h':
+ args.flags |= NO_FOLLOW;
+ break;
+ case 'm':
+ mtime = true;
+ break;
+ case 'p':
+ args.flags |= CREATE_PARENTS;
+ break;
+ case 'r':
+ rarg = optarg;
+ break;
+ case ':':
+ fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ case '?':
+ fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (marg) {
+ char *end;
+ long mode = strtol(marg, &end, 8);
+ // https://github.com/llvm/llvm-project/issues/64946
+ sanitize_init(&end);
+ if (*marg && !*end && mode >= 0 && mode < 01000) {
+ args.fmode = args.dmode = mode;
+ } else {
+ fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg);
+ return EXIT_FAILURE;
+ }
+ }
+
+ struct timespec times[2];
+
+ if (rarg) {
+ struct stat buf;
+ if (fstatat(AT_FDCWD, rarg, &buf, at_flags(&args)) != 0) {
+ fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, xstrerror(errno));
+ return EXIT_FAILURE;
+ }
+ times[0] = ST_ATIM(buf);
+ times[1] = ST_MTIM(buf);
+ } else if (darg) {
+ if (xgetdate(darg, &times[0]) != 0) {
+ fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, xstrerror(errno));
+ return EXIT_FAILURE;
+ }
+ times[1] = times[0];
+ } else {
+ // Don't use UTIME_NOW, so that multiple paths all get the same timestamp
+ if (xgettime(&times[0]) != 0) {
+ perror("xgettime()");
+ return EXIT_FAILURE;
+ }
+ times[1] = times[0];
+ }
+
+ if (!atime && !mtime) {
+ atime = true;
+ mtime = true;
+ }
+ if (atime) {
+ args.times[0] = times[0];
+ }
+ if (mtime) {
+ args.times[1] = times[1];
+ }
+
+ if (optind >= argc) {
+ fprintf(stderr, "%s: No files to touch\n", cmd);
+ return EXIT_FAILURE;
+ }
+
+ int ret = EXIT_SUCCESS;
+ for (; optind < argc; ++optind) {
+ const char *path = argv[optind];
+ if (xtouch(&args, path) != 0) {
+ fprintf(stderr, "%s: '%s': %s\n", cmd, path, xstrerror(errno));
+ ret = EXIT_FAILURE;
+ }
+ }
+ return ret;
+}