├── LICENSE ├── README.md ├── captain ├── captain.d ├── battery ├── clock ├── music ├── network ├── title ├── volume └── window ├── captainrc └── tools └── parser /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 A[muse]ment 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 | # Captain 2 | Everything you need to know can be found [here](https://github.com/muse/Captain/wiki). 3 | 4 | # Changelog 5 | ## Wed 2 Dec 02:11:47 UTC 2015 6 | * Renamed `updates` to `changelog` in the README.md 7 | * Renamed `bar-position` to `bar-format` 8 | * Removed `display`. You can now choose not to display them by not putting them in the `bar-format`. 9 | * Added manual parsing (hype). Including the documentation for it. 10 | * Added a feedback block send to stdout. 11 | * Finally reworked the structure -> Reference Wiki/HOME.md. 12 | * Minor changes to the code. 13 | * I likely forgot something, this point will be used as an memorial for the forgoten change. 14 | 15 | # Planned 16 | * Custom `process` name. 17 | * Multi monitor support. 18 | 19 | # The first start 20 | ![#](http://files.catbox.moe/2e7f5f.png) 21 | 22 | *It's not pretty, that's your job!* 23 | -------------------------------------------------------------------------------- /captain: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Author: Mirko van der Waal 3 | # Mail: 4 | # Distributed under terms of the MIT license. 5 | # 6 | # The MIT License (MIT) 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # Obviously we set -e to quit at anddiy error. 27 | set -e 28 | 29 | # Change the current directory to the captain directory always. 30 | # This would cause problems when the script is not ran from the captain directory. 31 | cd $(dirname $0) 32 | 33 | # XDG Compliance 34 | captaindir=${XDG_CONFIG_HOME:-~/.config}/captain 35 | 36 | # Create the log file, and bind it to a file descriptor. 37 | if [[ ! -d "$captaindir" ]]; then 38 | mkdir -p "$captaindir" 39 | fi && exec 3<> "$captaindir/captain.log" 40 | 41 | # Create the scripts directory. 42 | if [[ ! -d "$captaindir/captain.d" ]]; then 43 | mkdir -p "$captaindir/captain.d" 44 | fi && treasure="$captaindir/captain.d" 45 | 46 | manuscript="$captaindir/captainrc" 47 | 48 | # The only bit of python in the code, we parse .dosini file with it. 49 | source <(python3 -c " 50 | import sys, re 51 | from configparser import ConfigParser 52 | 53 | def variable(*fmt): 54 | return '{}_{}={}'.format(*fmt) 55 | 56 | def array(*fmt): 57 | return '{}_{}=({})'.format(*fmt) 58 | 59 | parser = ConfigParser() 60 | parser.read(sys.argv[1]) 61 | 62 | for section in parser: 63 | for key in parser[section]: 64 | # Split on spaces and newlines to determine when to create arrays. 65 | current = re.sub('[\n ]', '$', parser[section][key]).split('$') 66 | # Replace dashes with underscores for a better user experience. 67 | key = key.replace('-', '_') 68 | if len(current) == 1: 69 | print(variable(section, key, current[0])) 70 | else: 71 | print(array(section, key, ' '.join(current))) 72 | " $manuscript 2>&3) 2>&3 73 | 74 | # "It is easier to port a shell than a shell script." 75 | # -- Larry Wall 76 | 77 | export process=/tmp/captain-process 78 | export unique=/tmp/captain-stack 79 | 80 | # Always clean up when you are done. 81 | trap "rm -f $unique $process; trap - TERM; kill 0" INT ERR TERM QUIT 82 | 83 | # Retrieve a value from the stack, the stack is a collection of all scripts with 84 | # unique sets of characters that are used for event handles. 85 | stack() { 86 | { 87 | IFS=":" read -a char < <(grep -e "$1" < <(cat $unique)) 88 | sed -i "/$1:/d" $unique 89 | echo "${char[0]}:${char[1]:1:$((${#char[1]} + 1))}" >> $unique 90 | echo ${char[1]:0:1} 91 | } 2>&3 92 | } 93 | 94 | # Catch the strings send to stdout and execute the matching command. 95 | handle() { 96 | { 97 | while read -r line; do 98 | local cmd=${handle[$line]} 99 | if [[ ! -z $cmd ]]; then 100 | echo "$cmd &" 101 | fi 102 | done 103 | } 2>&3 104 | } 105 | 106 | # Dynamically format all the available options and display them accordingly. 107 | format() { 108 | { 109 | position=$(sed 's/:/ /g;s/|/%{c}/;s/|/%{r}/' <<< $bar_format) 110 | for script in $treasure/*; do 111 | position=$(sed "s/$(basename $script)/\$__$(basename $script)/g" <<< $position) 112 | switch="$switch $(basename $script)) __$(basename $script)=\${line#*@} ;;" 113 | done 114 | while read -r line; do 115 | eval "case ${line%%@*} in 116 | $switch 117 | esac" 118 | eval "echo \"$position\"" 119 | done 120 | } 2>&3 121 | } 122 | 123 | # A tiny parser that allows the manual usage of variables and events. 124 | manual() { 125 | { 126 | format=$(sed "s/<\#\([a-zA-Z0-9\_\-\ ]*\)\#>/\${$1_\1}/g" $2) 127 | for area in $(grep -Poe '(?<=<@).*?(?=@>)' <<< "$format"); do 128 | handle["$1${area}"]="$(eval "echo \${${name}_${area}}")" 129 | format=$(sed "s/<@\($area\)@>/$1${area}/g" <<< "$format") 130 | done 131 | content=$(tr '\n' ' ' < <(echo "$output" | eval "$format")) 132 | } 2>&3 133 | } 134 | 135 | # The defaults for the [bar] section. Most of the values are simplified and 136 | # require some extra attention to be formatted correctly. 137 | { 138 | declare -A bar=( 139 | [foreground]="#FFFFFFFF" 140 | [background]="#FF000000" 141 | [format]="||" 142 | [offset]=0 143 | [height]=20 144 | [length]='' 145 | [left]=0 146 | [top]=0 147 | [fonts]="monospace-9" 148 | [force]=true 149 | [dock]="top") 150 | for section in "${!bar[@]}"; do 151 | if [[ $(eval "echo \${#bar_${section}}") -eq 0 ]]; then 152 | echo "No value for 'bar-$section' -> using ${bar[${section}]}" 153 | eval "bar_${section}=${bar[${section}]}" 154 | fi 155 | done 156 | for section in $treasure/*; do 157 | printf "%s" "$(basename $section):" {A..E} $'\n' >> $unique 158 | done 159 | } 2>&3 160 | 161 | # Whenever a value is not defined, set the default value for it. Also looks for 162 | # manual mode. 163 | { 164 | declare -A choices=( 165 | [foreground]="${bar_foreground}" 166 | [background]="${bar_background}" 167 | [overline]=false 168 | [manual]=false 169 | [reload]=5) 170 | for file in $treasure/*; do 171 | name=$(basename $file) 172 | if [[ $(eval "echo \${${name}_manual") = true ]]; then 173 | echo "Manual mode has been enabled for ${name}." 174 | if [[ $(eval "echo \${#${name}_reload") -eq 0 ]]; then 175 | echo "No value for '${name}-$section' -> using ${choices[${section}]}" 176 | eval "${name}_reload=${choices[reload]}" 177 | fi 178 | break 179 | fi 180 | for section in "${!choices[@]}"; do 181 | if [[ $(eval "echo \${#${name}_${section}}") -eq 0 ]]; then 182 | echo "No value for '${name}-$section' -> using ${choices[${section}]}" 183 | eval "${name}_${section}=${choices[${section}]}" 184 | fi 185 | done 186 | done 187 | } 2>&3 188 | 189 | # Define what lines to draw (under & over), and what color they should be.. 190 | { 191 | declare -A line 192 | for file in $treasure/*; do 193 | name=$(basename $file) 194 | if [[ $(eval "echo \${#${name}_line}") -eq 0 ]]; then 195 | echo "No value for '${name}-line' -> using ${choices[foreground]}" 196 | eval "${name}_line=${choices[foreground]}" 197 | fi 198 | line["${name}"]="%{" 199 | for section in underline overline; do 200 | if [[ $(eval "echo \${${name}_${section}}") = true ]]; then 201 | line["${name}"]+="+${section:0:1}" 202 | else 203 | echo "No value for '${name}-${section}' -> using false" 204 | fi 205 | done 206 | line["${name}"]+="U$(eval "echo \${${name}_line}")}" 207 | done 208 | } 2>&2 209 | 210 | # Create event handles and add them in the area. 211 | { 212 | declare -A area handle events=( 213 | [mouse_middle]=2 214 | [scroll_down]=5 215 | [mouse_right]=3 216 | [mouse_left]=1 217 | [scroll_up]=4) 218 | for file in $treasure/*; do 219 | name=$(basename $file) 220 | for event in ${!events[@]}; do 221 | if [[ ! -z $(eval "echo \${${name}_${event}}") ]]; then 222 | uid="$(stack ${name})${name}" 223 | handle["${uid}"]="$(eval "echo \${${name}_${event}}")" 224 | area["${name}_p"]+="%{A${events[$event]}:${uid}:}" 225 | area["${name}_s"]+="%{A}" 226 | else 227 | echo "No events specified for '${name}-${event/_/-}' -> using none." 228 | fi 229 | done 230 | done 231 | } 2>&3 232 | 233 | # Parse our previously collected elements to a full string. 234 | { 235 | export content 236 | if [[ -e $process ]]; then 237 | rm $process 238 | fi && mkfifo "$process" 239 | for file in $treasure/*; do 240 | name=$(basename $file) 241 | if [[ ! -z $(tr -d [:digit:]. <<< $(eval "echo \${${name}_reload"})) ]]; then 242 | if [[ $(eval "echo \${${name}_manual}") = true ]]; then 243 | while read -r output; do 244 | manual "$name" "$file" 245 | printf "%s" \ 246 | "${name}@" \ 247 | "${content}" \ 248 | $'\n' > "$process" & 249 | done < <($(eval "echo \${${name}_reload"})) & 250 | else 251 | while read -r output; do 252 | printf "%s" \ 253 | "${name}@" \ 254 | "$(eval "echo \${area[${name}_p]"})" \ 255 | "$(eval "echo \${line[${name}]"})" \ 256 | "%{F$(eval "echo \${${name}_foreground}")}" \ 257 | "%{B$(eval "echo \${${name}_background"})}" \ 258 | "$(tr '\n' ' ' < <(bash "$file" "$output"))" \ 259 | "%{F-}%{B-}" \ 260 | "$(eval "echo \${area[${name}_s]"})" \ 261 | "%{-o}%{-uU-}" \ 262 | $'\n' > "$process" & 263 | done < <($(eval "echo \${${name}_reload"})) & 264 | fi 265 | else 266 | if [[ $(eval "echo \${${name}_manual}") = true ]]; then 267 | while :; do 268 | manual "$name" "$file" 269 | printf "%s" \ 270 | "${name}@" \ 271 | "${content}" \ 272 | $'\n' 273 | sleep $(eval "echo \${${name}_reload"}) 274 | done > "$process" & 275 | else 276 | while :; do 277 | printf "%s" \ 278 | "${name}@" \ 279 | "$(eval "echo \${area[${name}_p]"})" \ 280 | "$(eval "echo \${line[${name}]"})" \ 281 | "%{F$(eval "echo \${${name}_foreground}")}" \ 282 | "%{B$(eval "echo \${${name}_background"})}" \ 283 | "$(tr '\n' ' ' < <(bash < "$file"))" \ 284 | "%{F-}%{B-}" \ 285 | "$(eval "echo \${area[${name}_s]"})" \ 286 | "%{-o}%{-uU-}" \ 287 | $'\n' 288 | sleep $(eval "echo \${${name}_reload"}) 289 | done > "$process" & 290 | fi 291 | fi 292 | done 293 | } 2>&3 294 | 295 | # Information block. 296 | echo " 297 | ===== ===== ===== ===== ===== ===== 298 | $(date) - $(echo $USER) 299 | 300 | We found $(wc -l < <(ls $treasure)) scripts, we loaded roughly $( 301 | wc -w < <(uniq < <(sed 's/[|:]/\n/g' <<< $bar_format))) of them. 302 | 303 | You can limit the output visible by filtering it with 'grep'. 304 | If you want the corrections to be saved, pipe them to the log. 305 | 306 | If you're still confused, remember that the wiki exists: https://github.com/muse/Captain/wiki 307 | ===== ===== ===== ===== ===== =====" 308 | 309 | # Start captain. 310 | cat "$process" | format | lemonbar \ 311 | $(echo "-o $bar_offset") \ 312 | $(printf " -f %s" "${bar_fonts[@]}") \ 313 | $(echo "-${bar_dock//[^b]/}") \ 314 | $([[ $bar_force = true ]] && echo "-d") \ 315 | -g $bar_length\x$bar_height+$bar_left+$bar_top -F "$bar_foreground" -B "$bar_background" | handle | sh 316 | wait 317 | -------------------------------------------------------------------------------- /captain.d/battery: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Dependencies: acpi, siji 3 | 4 | percentage=$(acpi --battery | awk '{gsub(/,/, "");} {print $4}' | sed 's/.$//') 5 | 6 | case $percentage in 7 | [0-9]) token='' ;; 8 | [1-2][0-9]) token='' ;; 9 | [3-4][0-9]) token='' ;; 10 | [5-6][0-9]) token='' ;; 11 | [7-8][0-9]) token='' ;; 12 | 9[0-9]|100) token='' ;; 13 | esac 14 | if [[ $(acpi -a | grep on) ]]; then 15 | token='' 16 | fi 17 | 18 | echo "$token $percentage%" 19 | -------------------------------------------------------------------------------- /captain.d/clock: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Dependencies: * 3 | 4 | tr '[:lower:]' '[:upper:]' <<< $(date "+%I:%M %p") 5 | -------------------------------------------------------------------------------- /captain.d/music: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Dependencies: mpd, mpc, siji 3 | 4 | state=$(mpc status | grep -oe '\[[a-z]*\]') 5 | song=$(mpc current) 6 | 7 | if [[ $(wc -c <<< "$song") -gt 48 ]]; then 8 | song="$(cut -b 1-48 <<< "$song") ~" 9 | fi 10 | if [[ -z $state ]]; then 11 | token="" 12 | elif [[ $state == "[paused]" ]]; then 13 | token="" 14 | else 15 | token=""; 16 | fi 17 | 18 | echo "$token $song" 19 | -------------------------------------------------------------------------------- /captain.d/network: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Dependencies: iwconfig, siji 3 | 4 | wifi=$(cat /sys/class/net/wlp*/operstate) 5 | ethn=$(cat /sys/class/net/enp*/operstate) 6 | min=$(iwconfig 2>&1 | grep 'Link' | awk '{gsub(/[=/]/," "); print $3}') 7 | max=$(iwconfig 2>&1 | grep 'Link' | awk '{gsub(/[=/]/," "); print $4}') 8 | 9 | if [[ "$wifi" == "up" ]]; then 10 | percentage=$(bc <<< "$min * 100 / $max") 11 | case $percentage in 12 | [1-9] ) token="" ;; 13 | [1-3]*) token="" ;; 14 | [4-5]*) token="" ;; 15 | [6-7]*) token="" ;; 16 | *) token="" ;; 17 | esac 18 | elif [[ "$ethn" == "up" ]]; then 19 | token="" 20 | else 21 | token="" 22 | fi 23 | 24 | echo "$token" 25 | -------------------------------------------------------------------------------- /captain.d/title: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Dependencies: xdotool, xtitle 3 | # Description: Show the currenly active window. 4 | 5 | xtitle `xdotool getactivewindow` 6 | -------------------------------------------------------------------------------- /captain.d/volume: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Dependencies: amixer, siji 3 | 4 | volume=$(amixer sget Master | sed -n "0,/.*\[\([0-9]\+\)%\].*/s//\1/p") 5 | state=$(amixer sget Master | grep -Eoe '\[(on|off)\]') 6 | 7 | if [[ $volume -eq 0 || $state == '[off]' ]]; then 8 | token='' 9 | elif [[ $volume -gt 50 ]]; then 10 | token='' 11 | else 12 | token='' 13 | fi 14 | 15 | echo "$token $volume%" 16 | -------------------------------------------------------------------------------- /captain.d/window: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Dependencies: bspc, siji 3 | 4 | while read -r line; do 5 | case $line in 6 | W*) window="" 7 | IFS=':' 8 | set -- ${line#?} 9 | while [[ $# -gt 0 ]]; do 10 | case $1 in 11 | O*) window="${window}%{F<#focused#>}  %{F-}" 12 | ;; 13 | F*) window="${window}%{F<#focused#>}  %{F-}" 14 | ;; 15 | U*) window="${window}%{F<#focused#>}  %{F-}" 16 | ;; 17 | o*) window="${window}%{F<#passive#>}  %{F-}" 18 | ;; 19 | f*) window="${window}%{F<#passive#>}  %{F-}" 20 | ;; 21 | u*) window="${window}%{F<#passive#>}  %{F-}" 22 | ;; 23 | esac 24 | shift 25 | done 26 | ;; 27 | esac 28 | echo "$window" 29 | done 30 | -------------------------------------------------------------------------------- /captainrc: -------------------------------------------------------------------------------- 1 | ; A configuration file consists of sections, each led by a [section] header, 2 | ; followed by key/value entries separated by a specific string (= or : by default). 3 | ; By default, section names are case sensitive but keys are not. Leading and 4 | ; trailing whitespace is removed from keys and values. Values can be omitted, 5 | ; in which case the key/value delimiter may also be left out. Values can also 6 | ; span multiple lines, as long as they are indented deeper than the first line 7 | ; of the value. Depending on the parser’s mode, blank lines may be treated 8 | ; as parts of multiline values or ignored. 9 | 10 | ; Configuration files may include comments, prefixed by specific characters 11 | ; (# and ; by default). Comments may appear on their own on an otherwise 12 | ; empty line, possibly indented. 13 | 14 | ; The [bar] section is reserved and used by the bar script. It contains the 15 | ; variables bound to the script and should not be mirrored to any file in the 16 | ; captain.d directory. 17 | 18 | [bar] 19 | background = "#FFFFFFFF" 20 | foreground = "#FF000000" 21 | format = "window|title|clock" 22 | height = 20 23 | fonts = "monospace-9" 24 | "-wuncon-siji-medium-r-normal--10-100-75-75-c-80-iso10646-1" 25 | dock = "top" 26 | force = true 27 | offset = 0 28 | 29 | ; Anything below here is optional. 30 | 31 | ; Any manual script requires you to define 'manual = true'. All of the variables 32 | ; except reload are omited in the process. Other variables are to be placed manually 33 | ; by wrapping them in '<#' & '#>'. 34 | 35 | [window] 36 | manual = true 37 | reload = "bspc control --subscribe" 38 | 39 | ; You can use dashes, these will be converted to underscores later. This also 40 | ; means you need to reference to them with underscores in the script. 41 | 42 | passive = '#FFAF652F' 43 | focused = '#FFC8B55B' 44 | 45 | ; Events look the same, but are defined differently. You wrap them in '<@ & @>' 46 | ; A valid event would be '%{A1:<@event-name@>:}Hi!%{A}'. 47 | 48 | ; Example of normal scripts. 49 | 50 | [clock] 51 | background = "#FF000000" 52 | foreground = "#FFFFFFFF" 53 | overline = true 54 | scroll-up = "notify-send 'Scrolled up!'" 55 | scroll-down = "notify-send 'Scrolled down!'" 56 | mouse-middle = "notify-send 'Middle click!'" 57 | mouse-left = "notify-send 'Left click!'" 58 | mouse-right = "notify-send 'Right click!'" 59 | reload = 5 60 | 61 | [title] 62 | background = "#FF000000" 63 | foreground = "#FFFFFFFF" 64 | line = "#FF1134FF" 65 | overline = true 66 | underline = true 67 | scroll-up = "notify-send 'Scrolled up!'" 68 | scroll-down = "notify-send 'Scrolled down!'" 69 | mouse-middle = "notify-send 'Middle click!'" 70 | mouse-left = "notify-send 'Left click!'" 71 | mouse-right = "notify-send 'Right click!'" 72 | reload = "xtitle -s" 73 | 74 | # vim:syntax=dosini 75 | -------------------------------------------------------------------------------- /tools/parser: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # 3 | # After rewriting the parser, I decided to keep it here as a reference. 4 | # You can test run your config by passing it as an argument to the script. 5 | # 6 | # $ ./parser.py /path/to/captainrc 7 | # 8 | # Keep in mind the script does not rely on this file, the code is embedded in the daemon. 9 | 10 | import sys, re 11 | from configparser import ConfigParser 12 | 13 | def variable(*fmt): 14 | return "{}_{}={}".format(*fmt) 15 | 16 | def array(*fmt): 17 | return "{}_{}=({})".format(*fmt) 18 | 19 | parser = ConfigParser() 20 | parser.read(sys.argv[1]) 21 | 22 | for section in parser: 23 | for key in parser[section]: 24 | # Split on spaces and newlines to determine when to create arrays. 25 | current = re.sub('[\n ]', '$', parser[section][key]).split('$') 26 | # Replace dashes with underscores for a better user experience. 27 | key = key.replace('-', '_') 28 | if len(current) == 1: 29 | print(variable(section, key, current[0])) 30 | else: 31 | print(array(section, key, ' '.join(current))) 32 | --------------------------------------------------------------------------------