Skip to content

Commit 8a4dd5a

Browse files
committed
initial version
0 parents  commit 8a4dd5a

File tree

18 files changed

+1059
-0
lines changed

18 files changed

+1059
-0
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[*.sh]
2+
indent_style = tab
3+
indent_size = 8

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/coverage

.shellcheckrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
enable=all

.shellspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--require spec_helper

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
shellspec 0.28.1

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# 1.0.0
2+
3+
Initial version.

LICENSE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
dye is Copyright 2025 Mattie Behrens.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to one of the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# [dye](https://mattiebee.dev/dye)
2+
3+
- [About](#about)
4+
- [Usage](#usage)
5+
- [Development](#development)
6+
7+
## Demo
8+
9+
![A screenshot of the "demo" script in action](./doc/demo.png)
10+
11+
## About
12+
13+
dye is a **portable** and **respectful** library for adding color and emphasis to the output of shell scripts.
14+
15+
It's portable because
16+
17+
- it works on many Unix systems, including macOS, Linux, and OpenBSD;
18+
19+
- it is written to [the POSIX shell standard](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19), so it works in many shells that are POSIX-compatible, such as [ash and Dash](https://en.wikipedia.org/wiki/Almquist_shell), [Bash](https://www.gnu.org/software/bash/), [ksh](https://en.wikipedia.org/wiki/KornShell), and [Zsh](https://zsh.sourceforge.io);
20+
21+
- it uses [tput(1)](https://man.openbsd.org/tput) instead of hard-wired ANSI sequences, so it will work wherever the appropriate [terminal capabilities](https://man.openbsd.org/terminfo.5) are available, and gracefully degrade where they are not; and
22+
23+
- it additionally degrades gracefully if tput(1) is not available (e.g. in [a Docker alpine image](https://hub.docker.com/_/alpine) where ncurses is not installed).
24+
25+
It's respectful because:
26+
27+
- it will disable if ["NO_COLOR"](https://no-color.org) is set;
28+
29+
- it will enable if ["CLICOLOR"](https://bixense.com/clicolors/) is set, but only if stdout is a tty;
30+
31+
- it will unconditionally enable (e.g. if stdout is not a [tty](https://en.wikipedia.org/wiki/Tty_(Unix))) if "CLICOLOR_FORCE" is set;
32+
33+
- it allows script developers to choose to default to color off unless the user has opted in with environment variables; and
34+
35+
- it is written to only put functions and environment variables prefixed with "dye" or "DYE" into the shell's global namespace, and carefully avoids clobbering any existing shell variables during operation.
36+
37+
dye does call tput(1) for every terminal sequence it needs to output, so it's not screamingly fast. However, in practice, it's more than fast enough for the job I need it for: making the output of shell scripts colorful to make them easier to read and scan. If you're working with lots of color (for example, creating [ANSI art](https://en.wikipedia.org/wiki/ANSI_art)), it's probably best to stick to a solution that caches ANSI sequences and forgo the portability.
38+
39+
I wrote dye to replace my previous project, [portable-color](https://mattiebee.dev/portable-color). portable-color was fine, but would load lots of functions into the shell's global namespace. dye has a better API, with more capabilities and conveniences.
40+
41+
## Usage
42+
43+
### Embedding
44+
45+
You can use dye in your script by copying the contents of [dye.sh](./dye.sh) above the place you will use it.
46+
47+
This the method I recommend. Shell scripts that I write are generally made to be self-contained, and embedding makes them very easy to download and use.
48+
49+
A couple notes on this strategy:
50+
51+
- If you end up changing dye's code, consider changing the names of the dye functions and variables that end up in the shell's global namespace, to avoid conflicts with the standard dye code that may be depended on elsewhere—particularly if you're removing functionality you do not use.
52+
53+
- If you distribute your script with dye's code inline, you must include a copy of [the license](./LICENSE.md) in some way with your script. This specifically so others understand their rights in regard to the use of this software.
54+
55+
### Sourcing
56+
57+
If you have [dye.sh](./dye.sh) available [on your PATH](https://mattiebee.io/44251/a-proposal-for-shell-libraries) (executable bit not necessary), you can load it very simply:
58+
59+
```shell
60+
. dye.sh || exit 1
61+
```
62+
63+
If you include a copy alongside your script, you can also load it from a specific directory:
64+
65+
```shell
66+
. ./dye.sh || exit 1
67+
```
68+
69+
### Initialization
70+
71+
dye must be initialized once before use, or no text changes will happen when you use the color or emphasis routines:
72+
73+
```shell
74+
dye setup
75+
```
76+
77+
"setup" is where dye will check things like whether stdout is a tty, whether environment variables like "NO_COLOR" and "CLICOLOR" are set, and make a decision whether or not to set the variable "DYE_COLORS" to the number of available colors, which the other routines will use.
78+
79+
An alternate mode for "setup" will not enable color by default, but will enable it if the user has "CLICOLOR" set:
80+
81+
```shell
82+
dye setup default-off
83+
```
84+
85+
### Wrapping text
86+
87+
For simple [color](#colors) and [emphasis](#emphases), using dye to wrap quoted text is the most convenient method.
88+
89+
```shell
90+
echo "$(dye green "It's not easy being... well, you know.")"
91+
```
92+
93+
```shell
94+
echo "So $(dye bold "bold"), it's not recommended for human consumption\!"
95+
```
96+
97+
Quoting text is also not *strictly* necessary, but can result in the need to use many more escapes (just like it would if using "echo" straight up). It also means whitespace gets collapsed, so beware!
98+
99+
```shell
100+
echo Quotes\? Quotes\? $(dye italic We don\'t need no stinking quotes\!)
101+
```
102+
103+
#### Resets
104+
105+
When wrapping text, one key caveat applies: all colors and several emphases do not have a matching ending terminal sequence—they can only be turned off by sending an "sgr0" terminal capability to reset *all* color and emphasis.
106+
107+
dye will send this reset sequence at the end of wrapped text for colors and select emphases, so it's best not to stack wrappers. It gets unreadable really fast, anyway, so it's better to use [manual control](#manual-control).
108+
109+
### Manual control
110+
111+
More complex markup is easier to manage with manual control:
112+
113+
```shell
114+
echo "$(dye cyan)$(dye bold)Cyan$(dye reset), $(dye magenta)$(dye bold
115+
)magenta$(dye reset), and $(dye bold)white$(dye reset
116+
) ought to be enough for anybody."
117+
```
118+
119+
Using lots of manual control can make lines pretty long, but as you can see, you can also leverage the fact that line breaks are valid inside command substitution to break them up.
120+
121+
### Colors
122+
123+
Many colors are available for use, subject to terminal support.
124+
125+
There's the basic ANSI color set:
126+
127+
- `black` (or `0`)
128+
- `red` (or `1`)
129+
- `green` (or `2`)
130+
- `yellow` (or `3`)
131+
- `blue` (or `4`)
132+
- `magenta` (or `5`)
133+
- `cyan` (or `6`)
134+
- `white` (or `7`, or `brightgray`)
135+
- `gray` (or `8`)
136+
- `brightred` (or `9`)
137+
- `brightgreen` (or `10`)
138+
- `brightyellow` (or `11`)
139+
- `brightblue` (or `12`)
140+
- `brightmagenta` (or `13`)
141+
- `brightcyan` (or `14`)
142+
- `brightwhite` (or `15`)
143+
144+
"dye yellow" will set the foreground color to yellow, for example. There are also "fg" and "bg" commands that will explicitly set the foreground or background color, respectively:
145+
146+
```shell
147+
echo "$(dye bg blue)$(dye fg yellow)In Ann Arbor, everything is this color.$(dye reset)"
148+
```
149+
150+
#### High colors
151+
152+
Some terminal definitions, like "ansi" and "xterm", don't recognize colors higher than 7. If "DYE_COLORS" is 8, indicating this scenario, dye will synthesize "bright" colors by turning on "bold" and setting the non-bright equivalent.
153+
154+
Note that this also means that "bold" may be turned on unexpectedly if you're using "bright" colors—so keep this situation in mind:
155+
156+
- If you're nesting wrapped text, make sure that nested text deals with the fact that "bold" might be on if you're using a "bright" color.
157+
158+
- If you're using manual control, be sure to reset at the appropriate time if the possiblity that "bold" might be on.
159+
160+
You can test to see how your code is working by setting TERM to "ansi" or "xterm" on many systems.
161+
162+
### Emphases
163+
164+
Several emphases are available as well.
165+
166+
#### Resettable emphases
167+
168+
The first group, like colors, must be [reset](#resets) to turn them off:
169+
170+
- `dim` (makes things darker)
171+
- `bold`
172+
- `reverse` (see also "standout" below)
173+
174+
They can be used just like colors, and wrapping text with them will automatically send a reset at the end.
175+
176+
#### Endable emphases
177+
178+
The second group have "end" terminal sequences that can turn them off explicitly, with all other settings remaining in play:
179+
180+
- `italic` (or `i`)
181+
- `standout` (or `so`, often displayed as reversed foreground and background)
182+
- `underline` (or `ul`, or `u`)
183+
184+
When using one of these, you don't have to re-enable other modes:
185+
186+
```shell
187+
echo "$(dye magenta)Mary $(dye italic "had") a little lamb.$(dye reset)"
188+
echo "$(dye magenta)Mary had a $(dye italic "little") lamb.$(dye reset)"
189+
```
190+
191+
For manual control, the "end" command can be used for these:
192+
193+
```shell
194+
echo "Visit $(dye ul)https://mattiebee.dev/dye$(dye end ul) to get the code."
195+
```
196+
197+
To match "end", "begin" is also available (and works with all emphases). It behaves the same way as just using the emphasis, e.g. "dye begin italic" is equivalent to "dye italic".
198+
199+
## Development
200+
201+
### Testing
202+
203+
Unit tests are exhaustively written in [shellspec](https://shellspec.info). The [specs](./tools/specs) script will look for shells on your system that are expected to be compatible, and run the suite for each, stopping on the first failures.
204+
205+
[coverage](./tools/coverage) pairs shellspec with [kcov](http://simonkagstrom.github.io/kcov/) in [Docker](https://www.docker.com) to gather coverage while running against [Bash](https://www.gnu.org/software/bash/). (I'm using Docker here because I can't get shellspec with kcov to work on macOS.) 100% is impossible to reach due to kcov thinking some syntax isn't covered. But all meaningful lines of [dye.sh](./dye.sh) are covered.
206+
207+
### Standards
208+
209+
Code should all be written to [the POSIX shell spec](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html). Deviations are probably bugs. 🐜
210+
211+
This is particularly important because dye should run everywhere it can, including in very limited systems. If it starts getting loaded up with [Bashisms](https://www.bowmanjd.com/bash-not-bash-posix/), it won't work in some places.
212+
213+
### Notes
214+
215+
#### tput
216+
217+
During the development of dye, I did explore things like caching the output of tput(1) so it didn't have to be invoked quite so much.
218+
219+
The added complexity was really not worth it, since tput(1) is still fast enough (i.e. not at all noticeably slow) for most purposes where a shell script is doing work for at least a small amount of time. The cache would also need to be filled, and most scripts just don't switch colors enough to make it worthwhile.
220+
221+
The sequences dye generally uses are simple and unconcerned with this, but there are also interesting details with certain terminal control sequences on certain systems that tput(1) can handle if invoked directly, such as embedded delays. So, the practice also encourages maximum compatibility.

demo

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/bin/sh
2+
# shellcheck disable=SC2312
3+
#
4+
# Demos many of dye's features.
5+
#
6+
# The command use is intentionally varied, using wrapped mode or manual,
7+
# as well as several aliases for several emphases.
8+
#
9+
10+
set -eu
11+
12+
# shellcheck disable=SC1091
13+
. ./dye.sh
14+
15+
dye setup
16+
17+
cat <<EOT
18+
19+
$(dye so "| |")
20+
$(dye so "| dye's delightful demo |")
21+
$(dye so "| |")
22+
23+
dye is a $(dye bold "$(dye magenta "portable")"
24+
) and $(dye bold "$(dye cyan "respectful")"
25+
) color library for shell scripts.
26+
27+
It's $(dye bold "$(dye magenta "portable")"
28+
) because it should run anywhere a POSIX shell exists.
29+
30+
It's $(dye bold "$(dye cyan "respectful")"
31+
) because it carefully determines whether to enable
32+
color based on environment as well as user and developer preferences.
33+
$(dye italic "(Try this demo again with NO_COLOR set!)")
34+
35+
$(dye yellow)It has lots of colors and emphases:$(dye reset)
36+
37+
It lets you use many $(dye red "c"
38+
)$(dye yellow "o"
39+
)$(dye green "l"
40+
)$(dye cyan "o"
41+
)$(dye blue "r"
42+
)$(dye magenta "s"
43+
) as well as $(dye ul "several"
44+
) $(dye i "cool"
45+
) $(dye bold "text"
46+
) $(dye so "styles").
47+
48+
(And no, we didn't forget $(dye bg blue "$(dye yellow "background colors")")!)
49+
50+
$(dye dim "You can even do dark $(dye red "c")$(dye dim
51+
)$(dye yellow "o")$(dye dim
52+
)$(dye green "l")$(dye dim
53+
)$(dye cyan "o")$(dye dim
54+
)$(dye blue "r")$(dye dim
55+
)$(dye magenta "s")$(dye dim
56+
) if you want...")
57+
58+
$(dye yellow)It supports $(dye begin ul)256-color terminals$(dye end ul)*:$(dye reset)
59+
60+
EOT
61+
62+
print_range() (
63+
i="$1"
64+
end="$2"
65+
while [ "${i}" -le "${end}" ]; do
66+
dye fg "${i}"
67+
printf "%3s " "${i}"
68+
dye reset
69+
i=$((i + 1))
70+
done
71+
printf "\n"
72+
)
73+
74+
print_range 0 15
75+
printf "\n"
76+
77+
j=16
78+
while [ "${j}" -le 231 ]; do
79+
print_range "${j}" $((j + 17))
80+
j=$((j + 36))
81+
done
82+
printf "\n"
83+
84+
j=34
85+
while [ "${j}" -le 231 ]; do
86+
print_range "${j}" $((j + 17))
87+
j=$((j + 36))
88+
done
89+
printf "\n"
90+
91+
print_range 232 243
92+
print_range 244 255
93+
94+
cat <<EOT
95+
96+
$(dye i "*Not what you expected? Check your TERM environment variable.")
97+
98+
$(dye yellow)It can be used several ways:$(dye reset)
99+
100+
$(dye bold "$(dye blue "==>")") Wrapping quoted text: $(
101+
dye so "Like \$(dye green \"this\").")
102+
103+
$(dye bold "$(dye blue "==>")") Unquoted text: $(
104+
dye so "Like \$(dye green this, if whitespace is not important.)")
105+
106+
$(dye bold "$(dye blue "==>")") Manual: $(
107+
dye so "Like \$(dye green)this\$(dye reset).")
108+
109+
You can embed it or source it, depending on your needs.
110+
111+
You can also just use parts of it, like the enable-color detection.
112+
113+
And it's $(dye ul "completely tested") with $(dye bold "shellspec")!
114+
115+
$(dye brightblue)$(dye bold)https://mattiebee.dev/dye$(dye reset)
116+
117+
EOT

doc/demo.png

1.32 MB
Loading

0 commit comments

Comments
 (0)