├── Makefile ├── test ├── version.bats ├── help.bats └── test_helper.bash ├── package.json ├── LICENSE ├── README.md └── airport /Makefile: -------------------------------------------------------------------------------- 1 | BIN ?= airport 2 | PREFIX ?= /usr/local 3 | 4 | install: 5 | install $(BIN) $(PREFIX)/bin 6 | 7 | uninstall: 8 | rm -f $(PREFIX)/bin/$(BIN) 9 | -------------------------------------------------------------------------------- /test/version.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | @test "\`airport version\` returns with 0 status." { 6 | run "${_AIRPORT}" --version 7 | [[ "${status}" -eq 0 ]] 8 | } 9 | 10 | @test "\`airport version\` prints a version number." { 11 | run "${_AIRPORT}" --version 12 | printf "'%s'" "${output}" 13 | echo "${output}" | grep -q '\d\+\.\d\+\.\d\+' 14 | } 15 | 16 | @test "\`airport --version\` returns with 0 status." { 17 | run "${_AIRPORT}" --version 18 | [[ "${status}" -eq 0 ]] 19 | } 20 | 21 | @test "\`airport --version\` prints a version number." { 22 | run "${_AIRPORT}" --version 23 | printf "'%s'" "${output}" 24 | echo "${output}" | grep -q '\d\+\.\d\+\.\d\+' 25 | } 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airport-cli", 3 | "version": "0.7.4", 4 | "description": "A command line interface for AirPort wireless on macOS.", 5 | "global": true, 6 | "install": "make install", 7 | "bin": { 8 | "airport": "./airport" 9 | }, 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "test": "bats test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/xwmx/airport.git" 19 | }, 20 | "keywords": [ 21 | "macos", 22 | "airport", 23 | "wifi", 24 | "wireless", 25 | "osx", 26 | "apple", 27 | "cli", 28 | "terminal", 29 | "command-line", 30 | "prompt", 31 | "shell" 32 | ], 33 | "author": "William Melody", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/xwmx/airport/issues" 37 | }, 38 | "homepage": "https://github.com/xwmx/airport#readme", 39 | "dependencies": {} 40 | } 41 | -------------------------------------------------------------------------------- /test/help.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load test_helper 4 | 5 | _HELP_HEADER="\ 6 | _ __ 7 | ____ _(_)________ ____ _____/ /_ 8 | / __ \`/ / ___/ __ \\/ __ \\/ ___/ __/ 9 | / /_/ / / / / /_/ / /_/ / / / /_ 10 | \\__,_/_/_/ / .___/\\____/_/ \\__/ 11 | /_/" 12 | export _HELP_HEADER 13 | 14 | @test "\`help\` with no arguments exits with status 0." { 15 | run "${_AIRPORT}" help 16 | [ "${status}" -eq 0 ] 17 | } 18 | 19 | @test "\`help\` with no arguments prints default help." { 20 | run "${_AIRPORT}" help 21 | _compare "${_HELP_HEADER}" "$(IFS=$'\n'; echo "${lines[*]:0:6}")" 22 | [[ $(IFS=$'\n'; echo "${lines[*]:0:6}") == "${_HELP_HEADER}" ]] 23 | } 24 | 25 | @test "\`airport -h\` prints default help." { 26 | run "${_AIRPORT}" -h 27 | [[ $(IFS=$'\n'; echo "${lines[*]:0:6}") == "${_HELP_HEADER}" ]] 28 | } 29 | 30 | @test "\`airport --help\` prints default help." { 31 | run "${_AIRPORT}" --help 32 | [[ $(IFS=$'\n'; echo "${lines[*]:0:6}") == "${_HELP_HEADER}" ]] 33 | } 34 | -------------------------------------------------------------------------------- /test/test_helper.bash: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # test_helper.bash 3 | # 4 | # Test helper for Bats: Bash Automated Testing System. 5 | # 6 | # https://github.com/sstephenson/bats 7 | ############################################################################### 8 | 9 | setup() { 10 | # `$_AIRPORT` 11 | # 12 | # The location of the `pb` script being tested. 13 | export _AIRPORT="${BATS_TEST_DIRNAME}/../airport" 14 | } 15 | 16 | ############################################################################### 17 | # Helpers 18 | ############################################################################### 19 | 20 | # _compare() 21 | # 22 | # Usage: 23 | # _compare 24 | # 25 | # Description: 26 | # Compare the content of a variable against an expected value. When used 27 | # within a `@test` block the output is only displayed when the test fails. 28 | _compare() { 29 | local _expected="${1:-}" 30 | local _actual="${2:-}" 31 | printf "expected:\\n%s\\n" "${_expected}" 32 | printf "actual:\\n%s\\n" "${_actual}" 33 | } 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 William Melody 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _ __ 2 | ____ _(_)________ ____ _____/ /_ 3 | / __ `/ / ___/ __ \/ __ \/ ___/ __/ 4 | / /_/ / / / / /_/ / /_/ / / / /_ 5 | \__,_/_/_/ / .___/\____/_/ \__/ 6 | /_/ 7 | 8 | # airport cli 9 | 10 | A command line interface for AirPort wireless on macOS / OS X up to 11 | macOS Sonoma 14.4. 12 | 13 | **NOTE: Airport command line access is no longer available as of 14 | macOS Sonoma 14.4.** 15 | 16 | ## Usage 17 | 18 | ```text 19 | Usage: 20 | airport info [-l|--long] 21 | airport join 22 | airport quality 23 | airport off 24 | airport on 25 | airport scan [] 26 | airport ssid 27 | airport -h | --help | help 28 | airport --version 29 | 30 | Subcommands: 31 | info Print the current network SSID and quality. Use the -l or --long 32 | options to print detailed information. 33 | join Join the specified network. 34 | quality Show the wireless quality as a percentage. 35 | off Turn wireless off. 36 | on Turn wireless on. 37 | scan Perform a scan for wireless networks. 38 | ssid Print the current network's SSID. 39 | help Display this help information. 40 | 41 | Options: 42 | -h --help Display this help information. 43 | --version Display version information. 44 | ``` 45 | 46 | ## Installation 47 | 48 | ### Homebrew 49 | 50 | To install with [Homebrew](http://brew.sh/): 51 | 52 | ```bash 53 | brew tap xwmx/taps 54 | brew install airport 55 | ``` 56 | 57 | ### npm 58 | 59 | To install with [npm](https://www.npmjs.com/package/airport-cli): 60 | 61 | ```bash 62 | npm install --global airport-cli 63 | ``` 64 | 65 | ### bpkg 66 | 67 | To install with [bpkg](http://www.bpkg.io/): 68 | 69 | ```bash 70 | bpkg install xwmx/airport 71 | ``` 72 | 73 | ### Manual 74 | 75 | To install manually, simply add the `airport` script to your `$PATH`. If 76 | you already have a `~/bin` directory, you can use the following command: 77 | 78 | ```bash 79 | curl -L https://raw.github.com/xwmx/airport/master/airport \ 80 | -o ~/bin/airport && chmod +x ~/bin/airport 81 | ``` 82 | -------------------------------------------------------------------------------- /airport: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # _ __ 3 | # ____ _(_)________ ____ _____/ /_ 4 | # / __ `/ / ___/ __ \/ __ \/ ___/ __/ 5 | # / /_/ / / / / /_/ / /_/ / / / /_ 6 | # \__,_/_/_/ / .___/\____/_/ \__/ 7 | # /_/ 8 | # 9 | # A command line interface for Wi-Fi on macOS / OS X. 10 | # 11 | # https://github.com/xwmx/airport 12 | # 13 | # Based on Bash Boilerplate: https://github.com/xwmx/bash-boilerplate 14 | # 15 | # Copyright (c) 2015 William Melody • hi@williammelody.com 16 | 17 | ############################################################################### 18 | # Strict Mode 19 | ############################################################################### 20 | 21 | set -o nounset 22 | set -o errexit 23 | set -o pipefail 24 | IFS=$'\n\t' 25 | 26 | ############################################################################### 27 | # Environment 28 | ############################################################################### 29 | 30 | # $_VERSION 31 | # 32 | # The current program version. 33 | _VERSION="0.7.4" 34 | 35 | # $_ME 36 | # 37 | # Set to the program's basename. 38 | _ME=$(basename "${0}") 39 | 40 | # $_AIRPORT_FRAMEWORK_PATH 41 | # 42 | # The path to Apple's AirPort Wi-Fi framework on the local system. 43 | _AIRPORT_FRAMEWORK_PATH="/System/Library/PrivateFrameworks/Apple80211.framework" 44 | 45 | # $_AIRPORT_CMD 46 | # 47 | # The path to the `airport` command line tool. 48 | _AIRPORT_CMD="${_AIRPORT_FRAMEWORK_PATH}/Versions/A/Resources/airport" 49 | 50 | # $_NETWORKSETUP_CMD 51 | # 52 | # shellcheck disable=SC2230 53 | # https://github.com/koalaman/shellcheck/wiki/SC2230 54 | # See: "SC2230 - command -v is not a direct replacement for which #1162" 55 | # https://github.com/koalaman/shellcheck/issues/1162 56 | _NETWORKSETUP_CMD="$(which networksetup)" 57 | 58 | # $_WIFI_INTERFACE 59 | # 60 | # Command references: 61 | # - http://apple.stackexchange.com/a/90516 62 | # - https://jamfnation.jamfsoftware.com/discussion.html?id=4849 63 | _WIFI_INTERFACE="$( 64 | "${_NETWORKSETUP_CMD}" -listallhardwareports \ 65 | | awk '/Wi-Fi|AirPort/ {getline; print $NF}' 66 | )" 67 | 68 | ############################################################################### 69 | # Debug 70 | ############################################################################### 71 | 72 | # _debug() 73 | # 74 | # Usage: 75 | # _debug printf "Debug info. Variable: %s\n" "$0" 76 | # 77 | # A simple function for executing a specified command if the `$_USE_DEBUG` 78 | # variable has been set. The command is expected to print a message and 79 | # should typically be either `echo`, `printf`, or `cat`. 80 | __DEBUG_COUNTER=0 81 | _debug() { 82 | if [[ "${_USE_DEBUG:-"0"}" -eq 1 ]] 83 | then 84 | __DEBUG_COUNTER=$((__DEBUG_COUNTER+1)) 85 | # Prefix debug message with "bug (U+1F41B)" 86 | printf "🐛 %s " "${__DEBUG_COUNTER}" 87 | "${@}" 88 | printf "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n" 89 | fi 90 | } 91 | # debug() 92 | # 93 | # Usage: 94 | # debug "Debug info. Variable: $0" 95 | # 96 | # Print the specified message if the `$_USE_DEBUG` variable has been set. 97 | # 98 | # This is a shortcut for the _debug() function that simply echos the message. 99 | debug() { 100 | _debug echo "${@}" 101 | } 102 | 103 | ############################################################################### 104 | # Warn 105 | ############################################################################### 106 | 107 | # _warn() 108 | # 109 | # Usage: 110 | # _warn 111 | # 112 | # Description: 113 | # Print the specified command with output redirected to standard error. 114 | # The command is expected to print a message and should typically be either 115 | # `echo`, `printf`, or `cat`. 116 | _warn() { 117 | { 118 | printf "%s " "$(tput setaf 1)!$(tput sgr0)" 119 | 120 | "${@}" 121 | } 1>&2 122 | } 123 | 124 | ############################################################################### 125 | # Version Check 126 | ############################################################################### 127 | 128 | _MACOS_VERSION=("$(sw_vers -productVersion | awk -F. '{print $1 $2}')") 129 | 130 | if [[ "${_MACOS_VERSION[0]}" -ge "14" ]] && 131 | [[ "${_MACOS_VERSION[0]}" -ge "4" ]] 132 | then 133 | _warn printf \ 134 | "Airport command line access is no longer available as of macOS Sonoma 14.4.\\n\\n" 135 | "${_AIRPORT_CMD}" 136 | 137 | exit 1 138 | fi 139 | ############################################################################### 140 | # Options 141 | ############################################################################### 142 | 143 | # Parse Options ############################################################### 144 | 145 | # Initialize program option variables. 146 | _PRINT_HELP=0 147 | _PRINT_VERSION=0 148 | _USE_DEBUG=0 149 | 150 | # Initialize additional expected option variables. 151 | _SUBCOMMAND="info" 152 | _ARGUMENTS=("${0}") 153 | 154 | # getopts and getopts have inconsistent behavior, so using a simple home-brewed 155 | # while loop. This isn't perfectly compliant with POSIX, but it's close enough 156 | # and this appears to be a widely used approach. 157 | # 158 | # More info: 159 | # http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html 160 | # http://stackoverflow.com/a/14203146 161 | # http://stackoverflow.com/a/7948533 162 | while [[ ${#} -gt 0 ]] 163 | do 164 | opt="${1}" 165 | shift 166 | case "${opt}" in 167 | -h|--help|help) 168 | _PRINT_HELP=1 169 | ;; 170 | --debug) 171 | _USE_DEBUG=1 172 | ;; 173 | --version) 174 | _PRINT_VERSION=1 175 | ;; 176 | on|off|info|quality|scan|ssid|join|tool) 177 | _SUBCOMMAND="${opt}" 178 | ;; 179 | --endopts) 180 | # Terminate option parsing. 181 | break 182 | ;; 183 | *) 184 | _ARGUMENTS+=("${opt}") 185 | ;; 186 | esac 187 | done 188 | 189 | _debug printf "\${_SUBCOMMAND}: %s\\n" "${_SUBCOMMAND}" 190 | _debug printf "\${_ARGUMENTS[*]:-}: %s\\n" "${_ARGUMENTS[*]:-}" 191 | 192 | ############################################################################### 193 | # Program Functions 194 | ############################################################################### 195 | 196 | # --------------------------------------------------------------------- _help() 197 | # _help() 198 | # 199 | # Usage: 200 | # _help 201 | # 202 | # Description: 203 | # Print the program help information. 204 | _help() { 205 | cat < 218 | ${_ME} off 219 | ${_ME} on 220 | ${_ME} quality 221 | ${_ME} scan [] 222 | ${_ME} ssid 223 | ${_ME} -h | --help | help 224 | ${_ME} --version 225 | 226 | Subcommands: 227 | info Print the current network SSID and quality. Use the -l or --long 228 | options to print detailed information. 229 | join Join the specified network. 230 | quality Show the wireless quality as a percentage. 231 | off Turn wireless off. 232 | on Turn wireless on. 233 | scan Perform a scan for wireless networks. 234 | ssid Print the current network's SSID. 235 | help Display this help information. 236 | 237 | Options: 238 | -h --help Display this help information. 239 | --version Display version information. 240 | 241 | Home: 242 | https://github.com/xwmx/airport 243 | HEREDOC 244 | } 245 | 246 | # --------------------------------------------------------------------- _info() 247 | # _info() 248 | # 249 | # Usage: 250 | # _info 251 | # 252 | # Description: 253 | # Print the current network SSID and quality. 254 | _info() { 255 | local _ssid 256 | _ssid="$(_ssid)" 257 | 258 | local _quality 259 | if [[ -n "${_ssid:-}" ]] 260 | then 261 | _quality="$(_quality)" 262 | fi 263 | 264 | if [[ "${_ARGUMENTS[1]:-}" =~ ^-l|--help$ ]] 265 | then 266 | "${_AIRPORT_CMD}" --getinfo \ 267 | && [[ -n "${_quality:-}" ]] \ 268 | && printf " quality: %s\\n" "${_quality}" 269 | else 270 | if [[ -n "${_ssid}" ]] 271 | then 272 | printf "AirPort: On, Connected: %s (%s)\\n" "${_ssid}" "${_quality}" 273 | else 274 | printf "AirPort: On, Disconnected\\n" 275 | fi 276 | fi 277 | } 278 | 279 | # --------------------------------------------------------------------- _join() 280 | # _join() 281 | # 282 | # Usage: 283 | # _join 284 | # 285 | # Description: 286 | # Join the specified network. 287 | _join() { 288 | local _ssid="${1:-}" 289 | local _password= 290 | 291 | # Request password without displaying it 292 | printf "Password for \"%s\": " "${_ssid}" 293 | read -r -s _password 294 | printf "\\n" # print newline to stop `read`. 295 | 296 | _debug printf "airport() \${_WIFI_INTERFACE}: %s\\n" "${_WIFI_INTERFACE}" 297 | _debug printf "airport() \${_ssid}: %s\\n" "${_ssid}" 298 | 299 | "${_NETWORKSETUP_CMD}" \ 300 | -setairportnetwork \ 301 | "${_WIFI_INTERFACE}" \ 302 | "${_ssid}" \ 303 | "${_password}" 304 | } 305 | 306 | # ----------------------------------------------------------------------- _on() 307 | # _on() 308 | # 309 | # Usage: 310 | # _on 311 | # 312 | # Description: 313 | # Turn wireless on. 314 | _on() { 315 | "${_NETWORKSETUP_CMD}" -setairportpower "${_WIFI_INTERFACE}" on && \ 316 | printf "AirPort: On\\n" 317 | } 318 | 319 | # ---------------------------------------------------------------------- _off() 320 | # _off() 321 | # 322 | # Usage: 323 | # _off 324 | # 325 | # Description: 326 | # Turn wireless off. 327 | _off() { 328 | "${_NETWORKSETUP_CMD}" -setairportpower "${_WIFI_INTERFACE}" off && \ 329 | printf "AirPort: Off\\n" 330 | } 331 | 332 | # ------------------------------------------------------------------ _quality() 333 | # _quality() 334 | # 335 | # Usage: 336 | # _quality 337 | # 338 | # Description: 339 | # Print the wi-fi quality as a percent. 340 | _quality() { 341 | local _info 342 | local _signal 343 | local _noise 344 | local _snr 345 | local _percent 346 | 347 | # Reference: http://apple.stackexchange.com/a/110884 348 | _info="$("${_AIRPORT_CMD}" --getinfo)" 349 | _signal="$( 350 | printf "%s\\n" "${_info}" | awk '/agrCtlRSSI/ {print $NF}' 351 | )" 352 | _noise="$( 353 | printf "%s\\n" "${_info}" | awk '/agrCtlNoise/ {print $NF}' 354 | )" 355 | _snr="$(printf "100*(%s -(%s))\\n" "${_signal}" "${_noise}" | bc -l)" 356 | _percent="$(printf "%s/50\\n" "${_snr}" | bc -l)" 357 | printf "%s%%\\n" "${_percent%.*}" 358 | } 359 | 360 | # --------------------------------------------------------------------- _scan() 361 | # _scan() 362 | # 363 | # Usage: 364 | # _scan [] 365 | # 366 | # Description: 367 | # Perform a scan for wireless networks, optionally filtering by . 368 | _scan() { 369 | local _query="${1:-}" 370 | local _result= 371 | 372 | if [[ -n "${_query}" ]] 373 | then 374 | _result="$("${_AIRPORT_CMD}" --scan | grep "${_query}")" 375 | else 376 | _result="$("${_AIRPORT_CMD}" --scan)" 377 | fi 378 | 379 | if [[ -n "${_result}" ]] 380 | then 381 | printf "%s\\n" "${_result}" 382 | else 383 | return 1 384 | fi 385 | } 386 | 387 | # --------------------------------------------------------------------- _ssid() 388 | # _ssid() 389 | # 390 | # Usage: 391 | # _ssid 392 | # 393 | # Description: 394 | # Print the current network's SSID. 395 | _ssid() { 396 | "${_AIRPORT_CMD}" --getinfo \ 397 | | grep '[^B]SSID' \ 398 | | sed 's/.*SSID: //' 399 | } 400 | 401 | # --------------------------------------------------------------------- _tool() 402 | # _tool() 403 | # 404 | # Usage: 405 | # _tool 406 | # 407 | # Description: 408 | # Run a command using the `airport` framework command line tool directly. 409 | _tool() { 410 | "${_AIRPORT_CMD}" "$@" 411 | } 412 | 413 | # ------------------------------------------------------------------ _version() 414 | # _version() 415 | # 416 | # Usage: 417 | # _version 418 | # 419 | # Description: 420 | # Print the value of $_VERSION. 421 | _version() { 422 | printf "%s\\n" "${_VERSION}" 423 | } 424 | 425 | ############################################################################### 426 | # Main 427 | ############################################################################### 428 | 429 | _main() { 430 | if ((_PRINT_HELP)) 431 | then 432 | _help 433 | elif ((_PRINT_VERSION)) 434 | then 435 | _version 436 | else 437 | "_${_SUBCOMMAND}" "${_ARGUMENTS[@]:1}" 438 | fi 439 | } 440 | 441 | _main "${@:-}" 442 | --------------------------------------------------------------------------------