Added utility for parsing action Args param files (#2897)

This change introduces the `action_args` crate which is something I feel
I keep writing in various repos now. It's original design is to make it
easier to pass args to built binaries. E.g.

```rust
use action_args;
use clap::Parser;
use runfiles::{rlocation, Runfiles};

#[command(version, about, long_about = None)]
struct ClapArgs {}

fn main() {
    let args = {
        let runfiles = Runfiles::create().unwrap();

        let var = std::env::var("ARGS_FILE").unwrap();
        let runfile = rlocation!(runfiles, var).unwrap();
        let text = std::fs::read_to_string(runfile).unwrap();
        let argv = action_args::parse_args(text);
        ClapArgs::parse_from(std::env::args().take(1).chain(argv))
    };

    // ...
    // ...
    // ...
}
```

This utility will likely be unnecessary should
https://github.com/bazelbuild/bazel/issues/16076 ever be implemented.

Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
diff --git a/tools/action_args/BUILD.bazel b/tools/action_args/BUILD.bazel
new file mode 100644
index 0000000..10ac455
--- /dev/null
+++ b/tools/action_args/BUILD.bazel
@@ -0,0 +1,13 @@
+load("//rust:defs.bzl", "rust_library", "rust_test")
+
+rust_library(
+    name = "action_args",
+    srcs = ["action_args.rs"],
+    edition = "2021",
+    visibility = ["//visibility:public"],
+)
+
+rust_test(
+    name = "action_args_test",
+    crate = ":action_args",
+)
diff --git a/tools/action_args/action_args.rs b/tools/action_args/action_args.rs
new file mode 100644
index 0000000..0edf068
--- /dev/null
+++ b/tools/action_args/action_args.rs
@@ -0,0 +1,125 @@
+//! Utilities for parsing [Args](https://bazel.build/rules/lib/builtins/Args.html) param files.
+
+use std::path::Path;
+
+/// The format for an [Args param file[(https://bazel.build/rules/lib/builtins/Args.html#set_param_file_format).
+#[derive(Debug)]
+pub enum ActionArgsFormat {
+    /// Each item (argument name or value) is written verbatim to the param
+    /// file with a newline character following it.
+    Multiline,
+
+    /// Same as [Self::Multiline], but the items are shell-quoted.
+    Shell,
+
+    /// Same as [Self::Multiline], but (1) only flags (beginning with '--')
+    /// are written to the param file, and (2) the values of the flags, if
+    /// any, are written on the same line with a '=' separator. This is the
+    /// format expected by the Abseil flags library.
+    FlagPerLine,
+}
+
+impl Default for ActionArgsFormat {
+    fn default() -> Self {
+        Self::Shell
+    }
+}
+
+/// Parsed [`ctx.action.args`](https://bazel.build/rules/lib/builtins/Args.html) params.
+type ActionArgv = Vec<String>;
+
+/// Parse an [Args](https://bazel.build/rules/lib/builtins/Args.html) param file string into an argv list.
+pub fn parse_args_with_fmt(text: String, fmt: ActionArgsFormat) -> ActionArgv {
+    text.lines()
+        .map(|s| match fmt {
+            ActionArgsFormat::Shell => {
+                if s.starts_with('\'') && s.ends_with('\'') {
+                    s[1..s.len() - 1].to_owned()
+                } else {
+                    s.to_owned()
+                }
+            }
+            _ => s.to_owned(),
+        })
+        .collect()
+}
+
+/// Parse an [Args](https://bazel.build/rules/lib/builtins/Args.html) param file string into an argv list.
+pub fn parse_args(text: String) -> ActionArgv {
+    parse_args_with_fmt(text, ActionArgsFormat::default())
+}
+
+/// Parse an [Args](https://bazel.build/rules/lib/builtins/Args.html) param file into an argv list.
+pub fn try_parse_args_with_fmt(
+    path: &Path,
+    fmt: ActionArgsFormat,
+) -> Result<ActionArgv, std::io::Error> {
+    let text = std::fs::read_to_string(path)?;
+    Ok(parse_args_with_fmt(text, fmt))
+}
+
+/// Parse an [Args](https://bazel.build/rules/lib/builtins/Args.html) param file into an argv list.
+pub fn try_parse_args(path: &Path) -> Result<ActionArgv, std::io::Error> {
+    let text = std::fs::read_to_string(path)?;
+    Ok(parse_args(text))
+}
+
+#[cfg(test)]
+mod test {
+    use std::path::PathBuf;
+
+    use super::*;
+
+    const TEST_ARGS: [&str; 5] = ["foo", "-bar", "'baz'", "'--qux=quux'", "--quuz='corge'"];
+
+    #[test]
+    fn test_multiline_string() {
+        let text = TEST_ARGS.join("\n");
+
+        let args = parse_args_with_fmt(text, ActionArgsFormat::Multiline);
+        assert_eq!(
+            vec!["foo", "-bar", "'baz'", "'--qux=quux'", "--quuz='corge'"],
+            args
+        )
+    }
+
+    #[test]
+    fn test_shell_string() {
+        let text = TEST_ARGS.join("\n");
+
+        let args = parse_args_with_fmt(text, ActionArgsFormat::Shell);
+        assert_eq!(
+            vec!["foo", "-bar", "baz", "--qux=quux", "--quuz='corge'"],
+            args
+        )
+    }
+
+    #[test]
+    fn test_flag_per_line_string() {
+        let text = TEST_ARGS.join("\n");
+
+        let args = parse_args_with_fmt(text, ActionArgsFormat::FlagPerLine);
+        assert_eq!(
+            vec!["foo", "-bar", "'baz'", "'--qux=quux'", "--quuz='corge'"],
+            args
+        )
+    }
+
+    #[test]
+    fn test_from_file() {
+        let text = TEST_ARGS.join("\n");
+
+        let test_tempdir = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
+        let test_file = test_tempdir.join("test_from_file.txt");
+
+        assert!(try_parse_args(&test_file).is_err());
+
+        std::fs::write(&test_file, text).unwrap();
+
+        let args = try_parse_args(&test_file).unwrap();
+        assert_eq!(
+            vec!["foo", "-bar", "baz", "--qux=quux", "--quuz='corge'"],
+            args
+        )
+    }
+}