cowsay
Wrap text in a speech balloon and attach a cow to it
cowsay reads text, wraps it in a speech balloon, and draws an ASCII figure underneath saying
it. That is the whole program. It is here for the same reason wc is: it is a filter, it reads
standard input, and it composes with everything else.
It is not part of a base Debian install, so start with sudo apt install cowsay.
Debian puts it in /usr/games rather than /usr/bin, and that directory is on your PATH
at a login shell and on almost nothing else's, not cron's, not a systemd unit's, not a
docker exec's. A script that works when you run it by hand and reports cowsay: not found
from a cron job has hit that and nothing else, and the fix is the full path or a PATH= line at
the top of the crontab. The same list is visible from the other end on
sudo: command not found.
Input arrives one of two ways. Everything after the options is treated as the message, joined
with spaces, so cowsay Hello world and cowsay 'Hello world' produce the same balloon. With
no message argument it reads standard input instead, which is the form that matters: anything
that prints can be piped into it.
cowsay reflows. It does not print your text, it re-wraps it: line breaks in the input are discarded, the whole thing is treated as one
paragraph, and it is re-broken at 40 columns. Three lines in, one line out. -W changes the
width and -n turns wrapping off entirely, but -n works only when the text arrives on
standard input, and passing it a message argument prints the usage message instead of an error
worth reading.
The figure comes from a cowfile. cowsay -l lists the ones installed, -f picks one, and
$COWPATH is where it looks. A cowfile is not a picture: it is a small Perl program that
cowsay executes, which is why -f pointed at a file from the internet deserves the same
caution as any other downloaded script.
Sample files used on this page
Every example below was run against these files. Recreate them to follow along.
notes.txt three short lines, to show what wrapping does to them
Backup finished
14 files copied
0 errors
access.log 10 requests from 3 addresses, 7 of them 200s - the same sample log the text-processing pages use
203.0.113.5 - - [13/Aug/2026:09:12:01] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:12:03] "GET /style.css HTTP/1.1" 200 231
198.51.100.7 - - [13/Aug/2026:09:14:22] "GET /index.html HTTP/1.1" 200 512
198.51.100.7 - - [13/Aug/2026:09:14:25] "GET /missing.html HTTP/1.1" 404 162
203.0.113.5 - - [13/Aug/2026:09:15:47] "GET /index.html HTTP/1.1" 200 512
192.0.2.44 - - [13/Aug/2026:09:16:03] "POST /login HTTP/1.1" 302 0
192.0.2.44 - - [13/Aug/2026:09:16:04] "GET /dashboard HTTP/1.1" 200 4021
198.51.100.7 - - [13/Aug/2026:09:18:51] "GET /index.html HTTP/1.1" 200 512
203.0.113.5 - - [13/Aug/2026:09:19:10] "GET /api/status HTTP/1.1" 500 89
192.0.2.44 - - [13/Aug/2026:09:20:33] "GET /dashboard HTTP/1.1" 200 4021
cows/server.cow a hand-written cowfile, using each of the three variables the format provides
$the_cow = <<"EOC";
$thoughts
$thoughts
.-------------.
| $eyes ------ |
| $tongue |
'-------------'
\\___________/
EOC
downloaded.cow a cowfile that runs a command before drawing anything, standing in for one off the internet
print "(this line came from the cowfile, before any cow was drawn)\n";
$the_cow = <<"EOC";
$thoughts
$thoughts (a perfectly ordinary cow)
EOC
Saying something
Everything after the options is the message, joined with spaces. With no message at all, cowsay reads standard input instead - which is the form that makes it a filter rather than a toy.
Say a message
cowsay 'Hello, world'
The message goes in a balloon, sized to fit, with the default cow underneath.
Show output
______________
< Hello, world >
--------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Skip the quotes for plain words
cowsay Hello world
Arguments are joined with a single space, so quoting only matters when the message contains something the shell would act on - a ;, a $, a *, or the run of spaces you wanted kept.
Show output
_____________
< Hello world >
-------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Think it instead of saying it
cowthink 'Was that meant to happen?'
A second command from the same package, with a thought balloon and a trail of bubbles instead of a pointer. Every option below works identically on both.
Show output
___________________________
( Was that meant to happen? )
---------------------------
o ^__^
o (oo)\_______
(__)\ )\/\
||----w |
|| ||
Say nothing at all
cowsay ''
An empty message is not an error, and the balloon collapses to its minimum. Worth knowing when the thing you piped in turns out to have produced nothing.
Show output
__
< >
--
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Find where Debian put it
dpkg -L cowsay | grep /usr/games
/usr/games, not /usr/bin. That directory is on a login shell's PATH and not on cron's, which is the single most common reason a working command stops working inside a job.
Show output
/usr/games
/usr/games/cowsay
/usr/games/cowthink
Confirm which one your shell will run
command -v cowsay
Prints nothing and exits non-zero if /usr/games is missing from your PATH, which is a faster answer than reading the error from whatever invoked it.
Show output
/usr/games/cowsay
Taking input from a pipe
With no message argument cowsay reads standard input, so anything that prints can feed it. Remember that it re-wraps what it receives: the line breaks in the input are gone unless you pass -n.
Pipe a message in
echo 'Backup finished' | cowsay
The form almost every real use takes. cowsay behaves like any other filter here - see pipes and redirection if the plumbing is the unfamiliar part.
Show output
_________________
< Backup finished >
-----------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Read a file
cowsay < notes.txt
Input from a file is re-broken at 40 columns like anything else, so the three lines become one paragraph. That is rarely what you want for a file.
Show output
___________________________________
/ Backup finished 14 files copied 0 \
\ errors /
-----------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Keep the file's own line breaks
cowsay -n < notes.txt
-n turns wrapping off, so the input arrives as written. This is the fix for the previous example, and it works only because the text came in on standard input.
Show output
_________________
/ Backup finished \
| 14 files copied |
\ 0 errors /
-----------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Feed it a here-string
cowsay <<< 'from a here-string'
A bash here-string is standard input without a pipeline or a subshell, which matters when the next thing you want to do is read $? from the command that produced the text.
Show output
____________________
< from a here-string >
--------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Announce a count
wc -l < access.log | cowsay
< rather than a filename argument, so wc prints the number on its own instead of the number and the file name.
Show output
____
< 10 >
----
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Announce the result of a whole pipeline
awk '{print $1}' access.log | sort -u | wc -l | cowsay
Distinct client addresses in the log. cowsay is the last stage and cares only that something arrived on standard input, so any pipeline at all can end this way.
Show output
___
< 3 >
---
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Report a match count with a themed cow
grep -c ' 200 ' access.log | cowsay -f apt
-f apt picks the cowfile the cowsay package ships for Debian's own package manager. Cowfiles are covered below.
Show output
___
< 7 >
---
\ (__)
(oo)
/------\/
/ | ||
* /\---/\
~~ ~~
Preserve the columns of a counted list
uniq -c < notes.txt | cowsay -n
uniq -c right-aligns its counts, and without -n that alignment is destroyed along with the line breaks. Any command whose output is a table needs -n here.
Show output
_________________________
/ 1 Backup finished \
| 1 14 files copied |
\ 1 0 errors /
-------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Changing the face
Eight single-letter options set a preset expression, and two more set the eyes and tongue directly. They are mutually exclusive in practice: a preset always wins over -e, whichever order you write them in.
Assimilate the cow
cowsay -b 'Resistance is futile'
-b is "borg" mode, and sets the eyes to ==.
Show output
______________________
< Resistance is futile >
----------------------
\ ^__^
\ (==)\_______
(__)\ )\/\
||----w |
|| ||
Report a failure
cowsay -d 'The build is dead'
-d is "dead": xx for eyes, and a lolling U tongue. The most useful of the presets, because it reads as failure at a glance in a script that also uses the default cow for success.
Show output
___________________
< The build is dead >
-------------------
\ ^__^
\ (xx)\_______
(__)\ )\/\
U ||----w |
|| ||
Show greed
cowsay -g 'Disk usage: 98%'
-g is "greedy", with dollar signs for eyes.
Show output
_________________
< Disk usage: 98% >
-----------------
\ ^__^
\ ($$)\_______
(__)\ )\/\
||----w |
|| ||
Show paranoia
cowsay -p 'Who ran that as root?'
-p is "paranoid", with @@ for eyes.
Show output
_______________________
< Who ran that as root? >
-----------------------
\ ^__^
\ (@@)\_______
(__)\ )\/\
||----w |
|| ||
Show a cow that has had enough
cowsay -s 'Deploy went fine, probably'
-s is "stoned", and like -d it sets the tongue as well as the eyes.
Show output
____________________________
< Deploy went fine, probably >
----------------------------
\ ^__^
\ (**)\_______
(__)\ )\/\
U ||----w |
|| ||
Show tiredness
cowsay -t 'Third rebuild today'
-t is "tired", with half-closed -- eyes.
Show output
_____________________
< Third rebuild today >
---------------------
\ ^__^
\ (--)\_______
(__)\ )\/\
||----w |
|| ||
Show alertness
cowsay -w 'Tests passed'
-w is "wired", with wide OO eyes.
Show output
______________
< Tests passed >
--------------
\ ^__^
\ (OO)\_______
(__)\ )\/\
||----w |
|| ||
Show youth
cowsay -y 'First commit'
-y is "youthful", with small .. eyes.
Show output
______________
< First commit >
--------------
\ ^__^
\ (..)\_______
(__)\ )\/\
||----w |
|| ||
Set the eyes yourself
cowsay -e '^^' 'Custom eyes'
-e takes any two characters. Quote them: ^, $, * and @ all mean something to the shell first.
Show output
_____________
< Custom eyes >
-------------
\ ^__^
\ (^^)\_______
(__)\ )\/\
||----w |
|| ||
Watch a longer string get truncated
cowsay -e 'XXXX' 'Only the first two count'
Exactly two characters are used and the rest are dropped silently, because the cowfile has a two-character slot to fill. There is no error and no warning.
Show output
__________________________
< Only the first two count >
--------------------------
\ ^__^
\ (XX)\_______
(__)\ )\/\
||----w |
|| ||
Stick the tongue out
cowsay -T 'U ' 'Tongue out'
-T is the tongue, also two characters. The trailing space in 'U ' is deliberate: without it the second character comes from nowhere and the art shifts left.
Show output
____________
< Tongue out >
------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
U ||----w |
|| ||
Set both at once
cowsay -e '$$' -T 'vv' 'Both at once'
-e and -T are independent, so any combination works. Single quotes keep the shell away from the $$, which would otherwise expand to the shell's own process id.
Show output
______________
< Both at once >
--------------
\ ^__^
\ ($$)\_______
(__)\ )\/\
vv ||----w |
|| ||
Find out which wins
cowsay -d -e '^^' 'Which one wins?'
The preset does, in either order. If a script sets a mode from a variable and also wants custom eyes, only one of the two can be honoured - so pick -e and set both characters yourself.
Show output
_________________
< Which one wins? >
-----------------
\ ^__^
\ (xx)\_______
(__)\ )\/\
U ||----w |
|| ||
Combine a face with cowthink
cowthink -y -e 'oO' 'Thinking about it'
Same result - the preset overrides -e here too, and the only difference cowthink makes is the balloon and the bubbles.
Show output
___________________
( Thinking about it )
-------------------
o ^__^
o (..)\_______
(__)\ )\/\
||----w |
|| ||
Choosing a cow
The figure comes from a cowfile. -l lists what is installed, -f picks one by name, and $COWPATH is the colon-separated list of directories that -f searches - the same shape as PATH, and defaulting to /usr/share/cowsay/cows when it is unset.
List the cowfiles you have
cowsay -l
Names only, without the .cow extension, which is the form -f wants. The first line names the directory, so this doubles as a way to see what $COWPATH is currently resolving to.
Show output
Cow files in /usr/share/cowsay/cows:
apt bud-frogs bunny calvin cheese cock cower daemon default dragon
dragon-and-cow duck elephant elephant-in-snake eyes flaming-sheep fox
ghostbusters gnu hellokitty kangaroo kiss koala kosh luke-koala
mech-and-cow milk moofasa moose pony pony-smaller ren sheep skeleton
snowman stegosaurus stimpy suse three-eyes turkey turtle tux unipony
unipony-smaller vader vader-koala www
Count them
cowsay -l | tail -n +2 | wc -w
tail -n +2 drops the "Cow files in ..." heading, leaving only names for wc -w to count. Debian's cowsay package ships 47.
Show output
47
Pick one by name
cowsay -f tux 'Debian'
-f takes the name as -l printed it. The extension is optional - -f tux and -f tux.cow both work.
Show output
________
< Debian >
--------
\
\
.--.
|o_o |
|:_/ |
// \ \
(| | )
/'\_ _/`\
\___)=(___/
Use the cow the package manager gets
cowsay -f apt 'Reading package lists...'
Debian ships its own apt logo as a cowfile. It is the smallest of them, which makes it a good default when the output has to sit inside something else.
Show output
__________________________
< Reading package lists... >
--------------------------
\ (__)
(oo)
/------\/
/ | ||
* /\---/\
~~ ~~
Use a much larger one
cowsay -f stegosaurus 'Legacy code'
Cowfiles vary enormously in size. Check one before putting it in a login banner or a CI log, because nothing constrains the height and the balloon sits above whatever it is.
Show output
_____________
< Legacy code >
-------------
\ . .
\ / `. .' "
\ .---. < > < > .---.
\ | \ \ - ~ ~ - / / |
_____ ..-~ ~-..-~
| | \~~~\.' `./~~~/
--------- \__/ \__/
.' O \ / / \ "
(_____, `._.' | } \/~~~/
`----. / } | / \__/
`-. | / | / `. ,~~|
~-.__| /_ - ~ ^| /- _ `..-'
| / | / ~-. `-. _ _ _
|_____| |_____| ~ - . _ _ _ _ _>
See what a wrong name reports
cowsay -f nosuch 'hello'
A clear error and nothing else - no balloon, no fallback to the default cow. Anything less obvious than this means the file was found and something inside it went wrong.
Show output
cowsay: Could not find nosuch cowfile!
Use a cowfile by path
cowsay -f ./cows/server.cow 'Backup finished'
Any -f value containing a / is treated as a path and $COWPATH is skipped entirely. The leading ./ is not optional - see the last section for what happens without it.
Show output
_________________
< Backup finished >
-----------------
\
\
.-------------.
| oo ------ |
| |
'-------------'
\___________/
Point COWPATH at your own directory
COWPATH=$PWD/cows cowsay -f server 'Backup finished'
With $COWPATH set, -f takes a bare name again and finds your cowfile the same way it finds a shipped one. Set it in ~/.bashrc to make it permanent.
Show output
_________________
< Backup finished >
-----------------
\
\
.-------------.
| oo ------ |
| |
'-------------'
\___________/
List what a custom COWPATH offers
COWPATH=$PWD/cows cowsay -l | tail -1
-l walks every directory in $COWPATH and prints a heading for each. tail -1 here keeps the output to the names, since the headings are absolute paths specific to your machine.
Show output
server
Think with a chosen cow
cowthink -f tux 'Compiling'
-f works on cowthink unchanged. Only the balloon and the connector differ, so every cowfile has a thinking form for free.
Show output
___________
( Compiling )
-----------
o
o
.--.
|o_o |
|:_/ |
// \ \
(| | )
/'\_ _/`\
\___)=(___/
Pick a different cow every time
cowsay -f "$(cowsay -l | tail -n +2 | tr ' ' '\n' | shuf -n 1)" 'Surprise'
tr turns the space-separated list into one name per line so shuf -n 1 can choose one. Output differs every run, which is the point, so nothing is shown here.
Writing your own cowfile
A cowfile sets one variable, $the_cow, to the art. It is Perl, and the conventional form is an interpolating heredoc so that three variables can be substituted into the drawing: $eyes and $tongue (two characters each, from -e and -T) and $thoughts, which is \ for cowsay and o for cowthink.
Read the shipped default first
cat /usr/share/cowsay/cows/default.cow
The shipped default is eight lines and uses every convention the format has. Note that the backslashes in the art are doubled - the heredoc interpolates, so a single one would be consumed before the art ever reached the screen.
Show output
$the_cow = <<"EOC";
$thoughts ^__^
$thoughts ($eyes)\\_______
(__)\\ )\\/\\
$tongue ||----w |
|| ||
EOC
Read one you wrote
cat cows/server.cow
The same shape. $tongue is two spaces when -T is not given, which is why the line holding it is padded to look empty rather than short.
Show output
$the_cow = <<"EOC";
$thoughts
$thoughts
.-------------.
| $eyes ------ |
| $tongue |
'-------------'
\\___________/
EOC
Watch the variables fill in
cowsay -f ./cows/server.cow -d -T 'U ' 'Disk full'
-d writes into $eyes and -T into $tongue, in a cowfile that never mentions either option. Any cowfile that uses the variables gets every option on this page for nothing.
Show output
___________
< Disk full >
-----------
\
\
.-------------.
| xx ------ |
| U |
'-------------'
\___________/
Install it where cowsay looks
mkdir -p ~/.cows && cp cows/server.cow ~/.cows/
Nothing about ~/.cows is special to cowsay; it is just a directory you are about to name in $COWPATH. Keeping your cowfiles out of /usr/share means a package upgrade cannot remove them.
Make COWPATH permanent
echo 'export COWPATH=$HOME/.cows:/usr/share/cowsay/cows' >> ~/.bashrc
Your own directory comes first, so your cowfiles win a name clash and every shipped one still resolves. Single quotes so $HOME is written literally and expanded when the shell reads the file.
Controlling the wrapping
cowsay re-wraps everything it is given at 40 columns by default, joining the input into one paragraph first. -W changes the width; -n switches wrapping off, and works only on text arriving via standard input.
See the default width
cowsay 'The quick brown fox jumps over the lazy dog and keeps on running'
40 columns, which is narrow enough that most command output wraps. The balloon changes shape once it needs more than one line - the < > becomes a / \ and \ / frame.
Show output
_________________________________________
/ The quick brown fox jumps over the lazy \
\ dog and keeps on running /
-----------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Wrap at a narrower width
cowsay -W 20 'The quick brown fox jumps over the lazy dog'
Middle lines of a multi-line balloon are framed with | on both sides, so the shape tells you at a glance how many lines there are.
Show output
_____________________
/ The quick brown fox \
| jumps over the lazy |
\ dog /
---------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Wrap at a wider one
cowsay -W 70 'The quick brown fox jumps over the lazy dog and keeps on running'
Give it more than the text needs and the balloon goes back to a single line. -W is a maximum, not a fixed width.
Show output
__________________________________________________________________
< The quick brown fox jumps over the lazy dog and keeps on running >
------------------------------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Watch line breaks disappear
printf 'one\ntwo\nthree\n' | cowsay
The input's line breaks are gone, which is behind almost every "why does my output look wrong" question about cowsay, and it applies to anything piped in.
Show output
_______________
< one two three >
---------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Keep them
printf 'one\ntwo\nthree\n' | cowsay -n
-n passes the text through as it arrived. Nothing is wrapped, so a line longer than your terminal will still run off the edge - -n and -W are alternatives, not partners.
Show output
_______
/ one \
| two |
\ three /
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Using it in a script
The two things worth remembering when this moves out of your terminal: /usr/games is not on the PATH of anything scheduled, and cowsay writes to standard output like everything else, so it redirects and pipes normally.
Report success or failure differently
if make -s; then cowsay 'Build finished'; else cowsay -d 'Build failed'; fi
The dead cow for the failure branch is legible from across a room, which is the entire argument for doing this at the end of a long job. No output shown because it depends on your build.
Wrap it in a shell function
notify() { cowsay -W 60 "$*"; }
"$*" joins the arguments into one string, so notify deploy finished works without quotes at the call site. Put it in ~/.bashrc alongside your other functions.
Announce it from a git hook
printf '#!/bin/sh\ncowsay "Pushed to $(git branch --show-current)"\n' > .git/hooks/pre-push
Hooks run with your environment, so /usr/games is normally on the PATH here. Remember chmod +x - a hook that is not executable is skipped silently.
Give cron the full path
0 9 * * 1 /usr/games/cowsay 'Weekly report is due' | mail -s Reminder you
The reliable form inside a crontab, because cron's PATH is /usr/bin:/bin and nothing else. Setting PATH= at the top of the crontab works too.
See what cron would have seen
env -i PATH=/usr/bin:/bin sh -c 'cowsay Scheduled backup done'
env -i clears the environment so you can reproduce a scheduled job's PATH deliberately, rather than finding out at 09:00. The error comes from dash, which is what sh is on Debian.
Show output
sh: 1: cowsay: not found
Things that go wrong
Three failures that look like something else. The last is a security note rather than a cosmetic one, and it is the reason a cowfile from a stranger deserves the same reading as any other downloaded script.
Watch colour codes break the balloon
printf '\033[31mDeploy failed\033[0m on host deb1\n' | cowsay | cat -v
cowsay counts the escape sequences as characters when it sizes and pads the balloon, so the frame no longer lines up with the visible text. cat -v is only here to make the escapes printable on this page; strip colour before piping in, or accept a ragged edge.
Show output
____________________________
< ^[[31mDeploy failed^[[0m on host deb1 >
----------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Forget the ./ on a relative cowfile
cowsay -f cows/server.cow 'Backup finished'
A path with a / in it is handed to Perl's do, which will not read a relative path that does not start with ./. The balloon is drawn and the cow is not, and the error names Perl rather than cowsay.
Show output
do "cows/server.cow" failed, '.' is no longer in @INC; did you mean do "./cows/server.cow"? at /usr/games/cowsay line 196.
_________________
< Backup finished >
-----------------
Pass -n a message argument
cowsay -n 'why is this the usage message'
-n applies only to standard input, and giving it a message is treated as a usage error rather than reported as one. Nothing tells you which option it objected to.
Show output
cow{say,think} version 3.03, (c) 1999 Tony Monroe
Usage: cowsay [-bdgpstwy] [-h] [-e eyes] [-f cowfile]
[-l] [-n] [-T tongue] [-W wrapcolumn] [message]
Run a cowfile you did not write
cowsay -f ./downloaded.cow 'Nice cow'
A cowfile is executed, not parsed - cowsay loads it with Perl's do, so anything in the file runs with your privileges before a single character is drawn. Read a downloaded cowfile first; a legitimate one is a $the_cow assignment, a heredoc of art, and nothing else.
Show output
(this line came from the cowfile, before any cow was drawn)
__________
< Nice cow >
----------
\
\ (a perfectly ordinary cow)