I use unar for this; by default, if an archive contains more than one top-level file or directory, it creates a directory to store the extracted contents, named after the archive in the way you describe:
unar foo.zip
You can force the creation of a directory in all cases with the -d option:
unar -d foo.zip
Alternatively, a function can do this with unzip:
unzd() { if [[ $# != 1 ]]; then echo I need a single argument, the name of the archive to extract; return 1; fi target="${1%.zip}" unzip "$1" -d "${target##*/}" }
The
target=${1%.zip}
line removes the .zip extension, with no regard for anything else (so foo.zip becomes foo, and ~/foo.zip becomes ~/foo). The
${target##*/}
parameter expansion removes anything up to the last /, so ~/foo becomes foo. This means that the function extracts any .zip file to a directory named after it, in the current directory. Use unzip $1 -d "${target}" if you want to extract the archive to a directory alongside it instead.
unar is available for macOS (along with its GUI application, The Unarchiver), Windows, and Linux; it is packaged in many distributions, e.g. unar in Debian and derivatives, Fedora and derivatives, community/unarchiver in Arch Linux.
unzip -d foo foo.zip.