Best: probably that of Docker. https://github.com/docker/cli

Why: it didn't force you to read man pages or look up documentation, but instead allowed every command to explain what it does to you, either when you'd run it with --help, or just no parameters (in case it expects any). Furthermore, invocations of these commands weren't just a long string of arguments, but rather commands that are logically grouped and can essentially be navigated as a tree. All of that made it extremely useful and pleasant, at least in my eyes.

It just feels like it's made to actually be used by developers and to help them as much as possible. Whether you agree with me on that or not, i suggest that you have a look at this excellent talk by Dylan Beattie, "Life, Liberty and the Pursuit of APIness: The Secret to Happy Code", which talked more about the discoverability of systems and the developer experience: https://www.youtube.com/watch?v=lFRKrHE8oPo

Nowadays, you can actually use something like Typer for Python to create similarly useful interfaces, which i strongly advise you to have a brief look at: https://typer.tiangolo.com/

Example:

$ docker

Usage: docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Options:
... (a list of items)
-v, --version Print version information and quit

Management Commands:
... (a list of items)
image Manage images

Commands:
... (a list of items)

Run 'docker COMMAND --help' for more information on a command.

To get more help with docker, check out our guides at https://docs.docker.com/go/guides/

$ docker image

Usage: docker image COMMAND

Manage images

Commands:
... (a list of items)
ls List images
... (a list of items)
pull Pull an image or a repository from a registry

Run 'docker image COMMAND --help' for more information on a command.

$ docker image pull

"docker image pull" requires exactly 1 argument.
See 'docker image pull --help'.

Usage: docker image pull [OPTIONS] NAME[:TAG|@DIGEST]

Pull an image or a repository from a registry

$ docker image pull alpine:3.15

3.15: Pulling from library/alpine
59bf1c3509f3: Pull complete
Digest: sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300
Status: Downloaded newer image for alpine:3.15
docker.io/library/alpine:3.15

$ docker image ls

REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.15 c059bfaa849c 11 hours ago 5.59MB

The worst: tar

Why: https://xkcd.com/1168/

In short, it's the exact opposite of the previous example. Frankly, without memorizing the flags, i still have no idea how to work with archives with it. Say, i want to create a compressed archive with it.

Example:

$ tar

tar: You must specify one of the '-Acdtrux', '--delete' or '--test-label' options
Try 'tar --help' or 'tar --usage' for more information.

$ tar --usage

Usage: tar [-AcdrtuxGnSkUWOmpsMBiajJzZhPlRvwo?] [-g FILE] [-C DIR] [-T FILE]
[-X FILE] [-f ARCHIVE] [-F NAME] [-L NUMBER] [-b BLOCKS]
[-H FORMAT] [-V TEXT] [-I PROG] [-K MEMBER-NAME] [-N DATE-OR-FILE]
... (a really long list of items)

$ tar --help

Usage: tar [OPTION...] [FILE]...
GNU 'tar' saves many files together into a single tape or disk archive, and can
restore individual files from the archive.

Examples:
tar -cf archive.tar foo bar # Create archive.tar from files foo and bar.
tar -tvf archive.tar # List all files in archive.tar verbosely.
tar -xf archive.tar # Extract all files from archive.tar.

Main operation mode:
... (a list of items)
-c, --create create a new archive

Operation modifiers:
... (a list of items)

Local file name selection:
... (a list of items)

File name matching options (affect both exclude and include patterns):
... (a list of items)

Overwrite control:
... (a list of items)

Select output stream:
... (a list of items)

Handling of file attributes:
... (a list of items)

Handling of extended file attributes:
... (a list of items)

Device selection and switching:
... (a list of items)
-f, --file=ARCHIVE use archive file or device ARCHIVE

Device blocking:
... (a list of items)

Archive format selection:
... (a list of items)

FORMAT is one of the following:
... (a list of items)

Compression options:
... (a list of items)
-z, --gzip, --gunzip, --ungzip filter the archive through gzip

Local file selection:
... (a list of items)

File name transformations:
... (a list of items)

Informative output:
... (a list of items)
-v, --verbose verbosely list files processed

Compatibility options:
... (a list of items)

Other options:
... (a list of items)

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.

The backup suffix is '~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX.
The version control may be set with --backup or VERSION_CONTROL, values are:

... (a list of items)

Valid arguments for the --quoting-style option are:

... (a list of items)

*This* tar defaults to:
--format=gnu -f- -b20 --quoting-style=escape --rmt-command=/usr/lib/tar/rmt.exe
--rsh-command=/usr/bin/rsh

$ # Copied from the Internet, because the documentation is overwhelming
$ tar -vczf new-archive.tar.gz ./files-i-want-to-archive

./files-i-want-to-archive/
./files-i-want-to-archive/test1.txt
./files-i-want-to-archive/test2.txt
./files-i-want-to-archive/test3.txt

$ # Consider the full format instead, maybe we should actually use the full parameters more often?
$ tar --verbose --create --gzip --file=new-archive.tar.gz ./files-i-want-to-archive

In short, using tar does not inspire joy and it feels overcomplicated, no matter how you look at it, possibly either because creating archives is a complicated domain (though the zip tool might not necessarily support that claim), or because the tool has grown over time and no longer does just one thing and does it well.