Repository: clap-rs/clap (language: rust)

Issue:
Support externally supplying a terminal width when running outside a tty
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the [open](https://github.com/clap-rs/clap/issues) and [rejected](https://github.com/clap-rs/clap/issues?q=is%3Aissue+label%3AS-wont-fix+is%3Aclosed) issues

### Clap Version

v3.2.20

### Describe your use case

I am attempting to verify my readme contains the latest version of the help text from my utility. To do so I need an appropriately widthed help text to compare against (and auto-update from).

### Describe the solution you'd like

I'm not sure what the best interface for this would be. One thought I had was to obey the `COLUMNS` variable if that is set and the output is not a tty, that would make `text="$(COLUMNS=60 cargo run -- --help)"` work fine.

### Alternatives, if applicable

Locally I found a way to get a sub-tty with a limited width

```sh
script -q -c 'sh -c "stty cols 60 && cargo run -q -- --help"' $(mktemp)
```

but unfortunately that doesn't work in GitHub Actions for some unknown reason.

### Additional Context

_No response_


Discussion / hints:
Already [terminal_size](https://docs.rs/terminal_size/latest/terminal_size/fn.terminal_size.html) doesn't do detection if STDOUT is not a tty:

> Another option is to not use If STDOUT is not a tty, returns `None`

Is that sufficient or something going wrong with it?  Or are you wanting a different fallback terminal width?

Also, as a workaround, you can implement this all yourself by calling `terminal_size` directly and setting `term_width` under the specific circumstances for your app.
> Or are you wanting a different fallback terminal width?

Yes, this; I want to override the fallback terminal width in my script to have a consistent width no matter what terminal I'm running under.

> Also, as a workaround, you can implement this all yourself

Yep, it doesn't seem _too_ hard (just misses out on a lot of the `Parser` nicities to be able to override the derive). I just feel like it's pretty common to include `--help` output into a readme, where the default 100 column fallback is far too wide, so surely there must be other people running into this.
> Yes, this; I want to override the fallback terminal width in my script to have a consistent width no matter what terminal I'm running under.

In my initial research on this, it sounded like `COLUMNS` isn't actually env-variable but is instead something built directly into some shells that some users forward on to the environment and that application support is very half-hazard.  This made me less interested in supporting it.

Then I saw it suggested in https://clig.dev and saw it used in git which makes me feel more comfortable in supporting it.

The question is what should the level of precedence be?

1. COLUMNS
2. `terminal_size``
3. default

or

1. `terminal_size``
2. COLUMNS
3. default
Also, I feel safer putting this into clap v4 rather than clap v3 (if we make the release window)

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f8ef7725f8..b0a21b77f69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -254,6 +254,9 @@ Deprecated
 - *(derive)* `structopt` and `clap` attributes in favor of the more specific `command`, `arg`, and `value` to open the door for [more features](https://github.com/clap-rs/clap/issues/1807) and [clarify relationship to the builder](https://github.com/clap-rs/clap/discussions/4090) (#1807, #4180)
 - *(derive)* `#[clap(value_parser)]` and `#[clap(action)]` defaulted attributes (its the default) (#3976)
 
+Behavior Changes
+- *(help)* With `wrap_help` feature, if the terminal size cannot be determined, `LINES` and `COLUMNS` variables are used (#4186)
+
 ### Features
 
 - `Arg::num_args` now accepts ranges, allowing setting both the minimum and maximum number of values per occurrence (#2688, #4023)
@@ -270,6 +273,7 @@ Deprecated
 - `arg!(--long [value])` to accept `0..=1` per occurrence rather than across all occurrences, making it safe to use with `ArgAction::Append` (#4001)
 - Allow `OsStr`s for `Arg::{required_if_eq,required_if_eq_any,required_if_eq_all}` (#4084)
 - Can now pass runtime generated data to `Command`, `Arg`, `ArgGroup`, `PossibleValue`, etc without managing lifetimes
+- *(help)* With `wrap_help` feature, if the terminal size cannot be determined, `LINES` and `COLUMNS` variables are used (#4186)
 - *(help)* Use `Command::display_name` in the help title rather than `Command::bin_name`
 - *(help)* Show when a flag is `ArgAction::Count` by adding an `...` (#4003)
 - *(help)* Use a more neutral palette for coloring (#4132, #4117)
diff --git a/src/output/help.rs b/src/output/help.rs
index 0ef6934315d..c69920f1647 100644
--- a/src/output/help.rs
+++ b/src/output/help.rs
@@ -53,13 +53,15 @@ impl<'cmd, 'writer> Help<'cmd, 'writer> {
         let term_w = match cmd.get_term_width() {
             Some(0) => usize::MAX,
             Some(w) => w,
-            None => cmp::min(
-                dimensions().map_or(100, |(w, _)| w),
-                match cmd.get_max_term_width() {
+            None => {
+                let (current_width, _h) = dimensions();
+                let current_width = current_width.unwrap_or(100);
+                let max_width = match cmd.get_max_term_width() {
                     None | Some(0) => usize::MAX,
                     Some(mw) => mw,
-                },
-            ),
+                };
+                cmp::min(current_width, max_width)
+            }
         };
         let next_line_help = cmd.is_next_line_help_set();
 
@@ -960,12 +962,19 @@ fn option_sort_key(arg: &Arg) -> (usize, String) {
     (arg.get_display_order(), key)
 }
 
-pub(crate) fn dimensions() -> Option<(usize, usize)> {
+pub(crate) fn dimensions() -> (Option<usize>, Option<usize>) {
     #[cfg(not(feature = "wrap_help"))]
-    return None;
+    return (None, None);
 
     #[cfg(feature = "wrap_help")]
-    terminal_size::terminal_size().map(|(w, h)| (w.0.into(), h.0.into()))
+    terminal_size::terminal_size()
+        .map(|(w, h)| (Some(w.0.into()), Some(h.0.into())))
+        .unwrap_or_else(|| (parse_env("COLUMNS"), parse_env("LINES")))
+}
+
+#[cfg(feature = "wrap_help")]
+fn parse_env(var: &str) -> Option<usize> {
+    std::env::var_os(var)?.to_str()?.parse::<usize>().ok()
 }
 
 fn should_show_arg(use_long: bool, arg: &Arg) -> bool {


Test patch: