diff --git a/XFF.md b/XFF.md index cf5f77e..12bd0c1 100644 --- a/XFF.md +++ b/XFF.md @@ -10,6 +10,41 @@ xff walks each starting path and acts on the entries matching an expression, lik xff has two flavors selected by the program name: invoked as `find` it is strict find (only the standard vocabulary); invoked as `xff` it enables the modern extensions. An explicit `--config=find|xff` overrides the program name. Items marked as xff extensions below are the additions over find. +## Configuration + +xff configuration. Options resolve from layered config tiers, then the command line; later layers win. A style (find / xff / rg) sets the baseline defaults, which the tiers and the command line then adjust. Run `--explain` to print exactly what resolved. + +### Layers (lowest to highest precedence) + +- `system config` - machine-wide defaults (+ a root-owned [policy] that can hard-deny arming) +- `user config` - your personal defaults +- `--xffrc=FILE` - an explicitly named file (repeatable) - a NON-ARMING tier +- `command line` - flags and --config, highest + +There is no project / ancestor .xffrc discovery: config comes from the system and user files plus any `--xffrc` you name. `--no-config` ignores the discovered system/user files. + +### Choosing a style + +`--config=NAME` selects find / xff / rg (repeatable, last wins); see `--help=styles` for the table. The invocation name (argv[0]) is the leading selector, so a symlink named `find` runs the strict find style and `rg` the rg style; any other name (e.g. a `mytool` symlink) activates a same-named config block over the xff default. An explicit `--config` still stacks on top. + +### Arming dangerous directives + +A dangerous directive (the exec family -exec/-execdir/-ok/-capture, or -delete) carried by an --xffrc file is inert unless `--allow-exec` is set from a TRUSTED tier (the command line or the system/user config, never an --xffrc file itself). Unarmed lines are dropped with a warning; the root system [policy] can hard-deny even `--allow-exec`. + +### Config flags +- `--config=NAME` - select a config style: find (strict), xff (evolved), rg (opinionated); repeatable _(global, xff)_ + A config style sets the defaults for ignore files, hidden files, sizes, sort order, and case. find is strict find compatibility; xff keeps find's grammar but sorts and prints human sizes; rg is opinionated (respect .gitignore, skip hidden, smart case). Repeatable and layered, last one wins. See --help=styles for the per-style defaults. +- `--no-config` - ignore discovered .xffrc files _(global, xff)_ +- `--xffrc=FILE` - also load a specific config file (a non-arming tier; see --allow-exec) _(global, xff)_ + Loads FILE as a config tier above the user config (naming it is consent to LOAD it). It is a NON-ARMING tier: safe directives apply, but a dangerous one - the exec family (-exec/-execdir/-ok, -capture) or -delete - is inert unless --allow-exec is set from a trusted tier (the CLI or the user/system config, never from an --xffrc file itself). An unarmed dangerous line is dropped with a one-line warning. Repeatable; later files win. + Affects: --allow-exec + Affected by: --allow-exec +- `--allow-exec` - arm dangerous directives loaded from an --xffrc file (exec family, -delete) _(global, xff)_ + Permits the sensitive/destructive directives (the exec family -exec/-execdir/-ok and -capture, and the destructive -delete) carried by an --xffrc-loaded file to actually run. Honored only from a trusted tier - typed on the CLI, or set in the user/system config - never from an --xffrc file (so a named config cannot authorize itself). The root-owned system [policy] can hard-deny even this. Without it, such lines are inert (dropped + warned); -delete still obeys its own --safe/--dry-run guards. + Affects: --xffrc + Affected by: --xffrc +- `--explain` - print the resolved configuration and exit _(global, xff)_ + ## Options ### Config diff --git a/xff/cli/help.cc b/xff/cli/help.cc index 75c4661..737db44 100644 --- a/xff/cli/help.cc +++ b/xff/cli/help.cc @@ -168,46 +168,6 @@ std::string RenderExpressions() { return out; } -// The `--help=config` topic: how xff resolves options (layered tiers + the command line), how a -// style is chosen (--config and the argv[0] invocation name), and how dangerous --xffrc directives -// are armed. The config flags are pulled from the globals SOT via the "config" topic tag, so the -// flag list here cannot drift; the layering / argv[0] / arming rules are prose. -std::string RenderConfig() { - std::string out = - "xff configuration. Options resolve from layered config tiers, then the command line; later\n" - "layers win. A style (find / xff / rg) sets the baseline defaults, which the tiers and the\n" - "command line then adjust. Run --explain to print exactly what resolved.\n" - "\n" - "Layers, lowest to highest precedence:\n" - " system config machine-wide defaults (+ a root-owned [policy] that can hard-deny arming)\n" - " user config your personal defaults\n" - " --xffrc=FILE an explicitly named file (repeatable) - a NON-ARMING tier\n" - " command line flags and --config, highest\n" - "\n" - "There is no project / ancestor .xffrc discovery: config comes from the system and user files\n" - "plus any --xffrc you name. --no-config ignores the discovered system/user files.\n" - "\n" - "Choosing a style:\n" - " --config=NAME selects find / xff / rg (repeatable, last wins); see --help=styles for the table.\n" - " The invocation name (argv[0]) is the leading selector, so a symlink named `find` runs the\n" - " strict find style and `rg` the rg style; any other name (e.g. a `mytool` symlink) activates a\n" - " same-named config block over the xff default. An explicit --config still stacks on top.\n" - "\n" - "Arming dangerous directives:\n" - " A dangerous directive (the exec family -exec/-execdir/-ok/-capture, or -delete) carried by an\n" - " --xffrc file is inert unless --allow-exec is set from a TRUSTED tier (the command line or the\n" - " system/user config, never an --xffrc file itself). Unarmed lines are dropped with a warning;\n" - " the root system [policy] can hard-deny even --allow-exec.\n" - "\n" - "Config flags:\n"; - for (const GlobalFlag& flag : Globals()) { - if (flag.topic == "config") { - absl::StrAppend(&out, RenderGlobalFlag(flag, /*with_details=*/true), "\n"); - } - } - return out; -} - } // namespace // The `--help=cookbook` topic (aliases examples / recipes), folded into --help=full: task-oriented @@ -495,9 +455,6 @@ absl::StatusOr RenderHelp(std::string_view topic) { if (topic == "expressions") { return RenderExpressions(); // the annotated Tests/Actions/Operators list, sans globals } - if (topic == "config") { - return RenderConfig(); // config tiers, style selection, and arming - } if (topic == "cookbook" || topic == "examples" || topic == "recipes") { return RenderCookbook(); // worked examples composing the building blocks end to end } diff --git a/xff/cli/help_build.cc b/xff/cli/help_build.cc index 6592361..80c7516 100644 --- a/xff/cli/help_build.cc +++ b/xff/cli/help_build.cc @@ -331,6 +331,57 @@ Section StatsSection() { return section; } +// CONFIGURATION: how options resolve (layered tiers + the command line), how a style is +// chosen (--config / argv[0]), and how dangerous --xffrc directives are armed. The flags +// are pulled from the globals SOT via the "config" topic tag so the list cannot drift; +// the layering / argv[0] / arming rules are prose. Standalone as `--help=config` (see +// TopicReference) and folded into the full reference. +Section ConfigSection() { + Section section{.title = "Configuration"}; + section.children.push_back(ProseOf( + "xff configuration. Options resolve from layered config tiers, then the command line; later " + "layers win. A style (find / xff / rg) sets the baseline defaults, which the tiers and the " + "command line then adjust. Run `--explain` to print exactly what resolved.")); + + static constexpr std::array kLayers = {{ + {"system config", "machine-wide defaults (+ a root-owned [policy] that can hard-deny arming)"}, + {"user config", "your personal defaults"}, + {"--xffrc=FILE", "an explicitly named file (repeatable) - a NON-ARMING tier"}, + {"command line", "flags and --config, highest"}, + }}; + Subsection layers{.title = "Layers (lowest to highest precedence)"}; + layers.children.push_back(RowsOf(kLayers)); + layers.children.push_back(ProseOf( + "There is no project / ancestor .xffrc discovery: config comes from the system and user files " + "plus any `--xffrc` you name. `--no-config` ignores the discovered system/user files.")); + section.children.push_back(Content{.node = std::move(layers)}); + + Subsection style{.title = "Choosing a style"}; + style.children.push_back(ProseOf( + "`--config=NAME` selects find / xff / rg (repeatable, last wins); see `--help=styles` for the " + "table. The invocation name (argv[0]) is the leading selector, so a symlink named `find` runs the " + "strict find style and `rg` the rg style; any other name (e.g. a `mytool` symlink) activates a " + "same-named config block over the xff default. An explicit `--config` still stacks on top.")); + section.children.push_back(Content{.node = std::move(style)}); + + Subsection arming{.title = "Arming dangerous directives"}; + arming.children.push_back(ProseOf( + "A dangerous directive (the exec family -exec/-execdir/-ok/-capture, or -delete) carried by an " + "--xffrc file is inert unless `--allow-exec` is set from a TRUSTED tier (the command line or the " + "system/user config, never an --xffrc file itself). Unarmed lines are dropped with a warning; the " + "root system [policy] can hard-deny even `--allow-exec`.")); + section.children.push_back(Content{.node = std::move(arming)}); + + Subsection flags{.title = "Config flags"}; + for (const GlobalFlag& flag : Globals()) { + if (flag.topic == "config") { + flags.children.push_back(FlagEntry(flag)); + } + } + section.children.push_back(Content{.node = std::move(flags)}); + return section; +} + // EXAMPLES: the cookbook recipes as structured nodes - each a subsection with the // verbatim command (an Example, kept copy-pastable) and its explanation (Prose, which // wraps). The recipe list is the SOT in help.cc, run end to end by cookbook_test. @@ -476,6 +527,8 @@ std::optional TopicReference(std::string_view name) { doc.sections.push_back(GrammarsSection()); } else if (name == "stats") { doc.sections.push_back(StatsSection()); + } else if (name == "config") { + doc.sections.push_back(ConfigSection()); } else { return std::nullopt; } @@ -515,6 +568,7 @@ Document BuildReference() { }; doc.sections.push_back(DescriptionSection()); + doc.sections.push_back(ConfigSection()); doc.sections.push_back(OptionsSection(/*with_details=*/true)); doc.sections.push_back(ExpressionSection(/*with_details=*/true)); diff --git a/xff/cli/help_build_test.cc b/xff/cli/help_build_test.cc index 06e89ec..4a69b47 100644 --- a/xff/cli/help_build_test.cc +++ b/xff/cli/help_build_test.cc @@ -76,9 +76,10 @@ TEST_F(BuildReferenceTest, PreambleComesFromTheSot) { TEST_F(BuildReferenceTest, SectionsAppearInReferenceOrder) { EXPECT_THAT( - SectionTitles(doc), ElementsAre( - "Description", "Options", "Expression", "Fields", "Printf directives", "Time formats", - "Size units", "Regex grammars", "Statistics", "Examples", "Exit status", "See also")); + SectionTitles(doc), + ElementsAre( + "Description", "Configuration", "Options", "Expression", "Fields", "Printf directives", "Time formats", + "Size units", "Regex grammars", "Statistics", "Examples", "Exit status", "See also")); } TEST_F(BuildReferenceTest, ExpressionHasTheThreeKindSubsections) { diff --git a/xff/cli/help_render_test.cc b/xff/cli/help_render_test.cc index dd25cab..9e70dd0 100644 --- a/xff/cli/help_render_test.cc +++ b/xff/cli/help_render_test.cc @@ -172,7 +172,8 @@ TEST_F(HelpTest, EveryAdvertisedTopicRendersAndAliasesAreSynonyms) { // regex, or the shared DocRenderer walk that produces `fields`). for (const HelpTopic& topic : HelpTopics()) { if (topic.name == "styles" || topic.name == "fields" || topic.name == "printf" || topic.name == "time" - || topic.name == "size" || topic.name == "grammars" || topic.name == "extras" || topic.name == "stats") { + || topic.name == "size" || topic.name == "grammars" || topic.name == "extras" || topic.name == "stats" + || topic.name == "config") { continue; // rendered from the model (TopicReference) or the CLI facets, not RenderHelp } const absl::StatusOr rendered = RenderHelp(topic.name); @@ -206,6 +207,15 @@ TEST_F(HelpTest, StatsTopicDocumentsSummaryAndHistogram) { HasSubstr("--summary"), HasSubstr("--histogram"), HasSubstr("sum(lines)"), HasSubstr("needs an aggregator"))); } +TEST_F(HelpTest, ConfigTopicDocumentsTiersStyleAndArming) { + // `--help=config` renders from the model (TopicReference): the layered tiers, style + // selection (--config / argv[0]), and the arming rule for dangerous --xffrc directives. + EXPECT_THAT( + RenderTopicDoc("config"), AllOf( + HasSubstr("system config"), HasSubstr("command line"), HasSubstr("--config"), + HasSubstr("argv[0]"), HasSubstr("--allow-exec"))); +} + TEST_F(HelpTest, GlobalFlagTopicRendersWithGlobalTag) { EXPECT_THAT(RenderEntry("--sort"), AllOf(HasSubstr("--sort"), HasSubstr("ordering"), HasSubstr("global"))); }