├── LICENSE ├── README.md ├── i3bang.rb └── test.rb /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Keyboard Fire 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i3bang 2 | 3 | A preprocessor for i3 config files aimed to drastically reduce their length. 4 | 5 | ## Examples 6 | 7 | A full config file that employs heavy use of i3bang [can be found in my dotfiles 8 | repo](https://github.com/KeyboardFire/dotfiles/blob/master/.i3/_config), and 9 | indeed is my own. In my obviously unbiased and completely objective opinion, it 10 | is elegant and beautiful. ;) 11 | 12 | Here are a few select snippets: 13 | 14 | --- 15 | 16 | Firstly, you're probably going to want to put this in your config file: 17 | 18 | #!nobracket 19 | 20 | This allows you to use i3bang without angle brackets (`<>`) and instead use 21 | no open delimiter and whitespace as a "close delimiter." This is typically what 22 | you want, and it's only not set by default for backwards compatibility. 23 | 24 | Very simple expansions: 25 | 26 | bindsym !!Return,Escape,space,$mod+r mode "default" 27 | 28 | becomes 29 | 30 | bindsym Return mode "default" 31 | bindsym Escape mode "default" 32 | bindsym space mode "default" 33 | bindsym $mod+r mode "default" 34 | 35 | --- 36 | 37 | Dual expansions and line continuation: 38 | 39 | bindsym $mod+!!q,w,e \ # change container layout 40 | layout !! 41 | 42 | becomes 43 | 44 | bindsym $mod+q layout stacking 45 | bindsym $mod+w layout tabbed 46 | bindsym $mod+e layout toggle split 47 | 48 | --- 49 | 50 | Separate expansions: 51 | 52 | bindsym $mod!!<2!,+Shift>+!!1!1..9,0 \ # workspaces! 53 | !!<2!,move container to >\ 54 | workspace number !!1!1..10 55 | 56 | becomes 57 | 58 | bindsym $mod+1 workspace number 1 59 | bindsym $mod+2 workspace number 2 60 | bindsym $mod+3 workspace number 3 61 | bindsym $mod+4 workspace number 4 62 | bindsym $mod+5 workspace number 5 63 | bindsym $mod+6 workspace number 6 64 | bindsym $mod+7 workspace number 7 65 | bindsym $mod+8 workspace number 8 66 | bindsym $mod+9 workspace number 9 67 | bindsym $mod+0 workspace number 10 68 | bindsym $mod+Shift+1 move container to workspace number 1 69 | bindsym $mod+Shift+2 move container to workspace number 2 70 | bindsym $mod+Shift+3 move container to workspace number 3 71 | bindsym $mod+Shift+4 move container to workspace number 4 72 | bindsym $mod+Shift+5 move container to workspace number 5 73 | bindsym $mod+Shift+6 move container to workspace number 6 74 | bindsym $mod+Shift+7 move container to workspace number 7 75 | bindsym $mod+Shift+8 move container to workspace number 8 76 | bindsym $mod+Shift+9 move container to workspace number 9 77 | bindsym $mod+Shift+0 move container to workspace number 10 78 | 79 | --- 80 | 81 | Variables and sections: 82 | 83 | !@<*mousemove 84 | exec xdotool mousemove_relative !!1!-!s,0,0,!s,-!s,!s,-!s,!s \ 85 | !!1!0,!s,-!s,-,-!s,-!s,!s,!s> 86 | mode "mouse" { 87 | !s = 20 # hjkl speed 88 | bindsym !!1!h,j,k,l,y,u,b,n !@mousemove 89 | !s = 8 # numpad speed 90 | bindsym KP_!!1!4,2,8,6,7,9,1,3 !@mousemove 91 | 92 | becomes 93 | 94 | mode "mouse" { 95 | bindsym h exec xdotool mousemove_relative -20 0 96 | bindsym j exec xdotool mousemove_relative 0 20 97 | bindsym k exec xdotool mousemove_relative 0 -20 98 | bindsym l exec xdotool mousemove_relative 20 - 99 | bindsym y exec xdotool mousemove_relative -20 -20 100 | bindsym u exec xdotool mousemove_relative 20 -20 101 | bindsym b exec xdotool mousemove_relative -20 20 102 | bindsym n exec xdotool mousemove_relative 20 20 103 | bindsym KP_4 exec xdotool mousemove_relative -8 0 104 | bindsym KP_2 exec xdotool mousemove_relative 0 8 105 | bindsym KP_8 exec xdotool mousemove_relative 0 -8 106 | bindsym KP_6 exec xdotool mousemove_relative 8 - 107 | bindsym KP_7 exec xdotool mousemove_relative -8 -8 108 | bindsym KP_9 exec xdotool mousemove_relative 8 -8 109 | bindsym KP_1 exec xdotool mousemove_relative -8 8 110 | bindsym KP_3 exec xdotool mousemove_relative 8 8 111 | 112 | --- 113 | 114 | Environment variable conditionals: 115 | 116 | !? 119 | 120 | This will only add the content inside the conditional to your config file if 121 | i3bang is run by user "someusername." 122 | 123 | --- 124 | 125 | Raw sections: 126 | 127 | !@<+default_keybindings 128 | ... # (you can have !! and ! inside here) 129 | > 130 | 131 | mode "foo" { 132 | ... 133 | !@default_keybindings 134 | } 135 | 136 | This allows you to keep your default mode keybindings in different modes. When 137 | you prepend a `+` to a section's name, it is interpreted as meaning "raw mode," 138 | which means that a.) all `!!` and `!` is treated as it 139 | normally would, and b.) only a `>` on its own line can end the section 140 | (allowing you to still be able to use `!!`/`!` with brackets. 141 | Hence, you can simply wrap all your default keybindings in a 142 | `!@<+default_keybindings ... >`, and then stick a `!@default_keybindings` at 143 | the end of every mode that you want to keep them in (put it at the end because 144 | for some reason keybindings that come *first* take precedence and override 145 | bindings to the same key that come later). 146 | 147 | ## Usage 148 | 149 | 1. Place `i3bang.rb` in your `~/.i3` (copy it there, move it there, or make a 150 | symlink). 151 | 152 | 2. Rename your `config` file to something else (mine is `_config`). 153 | 154 | 3. When you want to generate the normal `config` file, run `./i3bang.rb 155 | _config` (or whatever you named your non-preprocessed config file). 156 | 157 | 4. (recommended) Change this line in your i3 config file: 158 | 159 | bindsym $mod+Shift+c reload 160 | 161 | to this: 162 | 163 | bindsym $mod+Shift+c exec ~/.i3/i3bang.rb _config; reload 164 | 165 | in order to automatically preprocess the config file whenever you reload 166 | your config (hit mod+shift+c). 167 | 168 | ## Advanced Examples 169 | 170 | Advanced line continuations + expansions, plus math: 171 | 172 | bindsym Alt_L !!<\> 173 | exec echo !!1..9,0 | dzen2 -x 175 -y !<55+40*!!<0..9>> -w 30 -h 30 -p 1; !!<\> 174 | nop 175 | 176 | becomes 177 | 178 | bindsym Alt_L exec echo 1 | dzen2 -x 175 -y 55 -w 30 -h 30 -p 1; exec echo 2 | dzen2 -x 175 -y 95 -w 30 -h 30 -p 1; exec echo 3 | dzen2 -x 175 -y 135 -w 30 -h 30 -p 1; exec echo 4 | dzen2 -x 175 -y 175 -w 30 -h 30 -p 1; exec echo 5 | dzen2 -x 175 -y 215 -w 30 -h 30 -p 1; exec echo 6 | dzen2 -x 175 -y 255 -w 30 -h 30 -p 1; exec echo 7 | dzen2 -x 175 -y 295 -w 30 -h 30 -p 1; exec echo 8 | dzen2 -x 175 -y 335 -w 30 -h 30 -p 1; exec echo 9 | dzen2 -x 175 -y 375 -w 30 -h 30 -p 1; exec echo 0 | dzen2 -x 175 -y 415 -w 30 -h 30 -p 1; nop 179 | 180 | Note that `!!<\>` acts as a line continuation that is only applied after 181 | expansions have been... expanded. This is helpful for expanding many actions to 182 | a single `bindsym`, which requires each separate command to be 183 | semicolon-separated on the same line. 184 | -------------------------------------------------------------------------------- /i3bang.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | class I3bangError < RuntimeError; end 4 | 5 | def i3bang config, header = '' 6 | 7 | # kill comments; bangs in them interfere 8 | # also annoying trailing whitespace 9 | nobracket = config.include? '#!nobracket' 10 | config.gsub! /\s*#*#[! ](?!i3 config).*\n/, "\n" 11 | config.gsub! /\s+$/, '' 12 | 13 | # line continuations 14 | config.gsub! /\\\n\s*/, '' 15 | config += "\n" # add back trailing newline 16 | 17 | # add notice 18 | # (feel free to remove/edit this; I don't mind) 19 | config = header + config 20 | 21 | # change shorthand format to expanded, regular format if specified 22 | # UGLY HACK WARNING: !!<...!...> can interfere, so we're going to take 23 | # all existing ![@!]?<...>'s out and put them back in later. 24 | # BUT, expansions might contain !... and !!...s as well, so we have to rerun 25 | # this upon every expansion. Therefore, this is stuck inside a method. 26 | def expand_nobracket config 27 | placeholder = '__PLCHLD__' 28 | ph_arr = [] 29 | n = -1 30 | config.gsub!(/ 31 | # nested brackets: 32 | (?:!\?<|!@<+) # conditionals or sections in raw mode 33 | [\s\S]*? # anything, including newlines 34 | \n>(?=\n) # bracket on its own line 35 | | 36 | # non-nested sections: 37 | !@< 38 | [^>+][^>]* # no + because that signifies raw mode 39 | > 40 | | 41 | # "regular" brackets (!<...> and !!<...>): 42 | !!?< 43 | [^>]* 44 | > 45 | /x) {|m| 46 | n += 1 47 | # but we still want to expand !'s in !@ 48 | if m[1] == '@' 49 | m = m[3..-2] 50 | expand = false 51 | if m[0] == '+' 52 | expand = true 53 | m = m[1..-1] 54 | end 55 | expand_nobracket m if expand 56 | m = '!@<' + m + '>' 57 | end 58 | ph_arr.push m 59 | placeholder + "<#{n}>" 60 | } 61 | # now the actual substitutions 62 | # bangs at the beginning of the line - we always expand these, even 63 | # when whitespace follows them 64 | config.gsub!(/ 65 | ^\s* # beginning of line, with possible whitspace 66 | (![@!?]?) # the type of thing it is 67 | ( 68 | [^!\n]* # exclude !'s because there could be two 69 | # separate !<...>'s on one line 70 | )$ # end of line, we want to capture everything 71 | /x, '\1<\2>') 72 | # inline bangs, we stop at whitespace for these 73 | config.gsub!(/ 74 | (![@!?]?) # the type of thing it is 75 | ( 76 | [^<@!?\s] # we have to exclude: 77 | # - < to avoid adding brackets where they already exist 78 | # - @!? to avoid premature bracket-adding 79 | # (ex. !!foo -> !) 80 | \S* # grab everything until whitespace is found 81 | ) 82 | /x, '\1<\2>') 83 | # replace the placeholders 84 | config.gsub!(/#{placeholder}<(\d+)>/) { ph_arr[$1.to_i] } 85 | end 86 | expand_nobracket config if nobracket 87 | 88 | # first check for !?<...> environment variable conditionals 89 | config.gsub!(/!\?<([\s\S]*?\n)>(?=\n)/) { 90 | condition, data = $1.split "\n", 2 91 | raise I3bangError, 'insufficient argumets for conditional' if condition.nil? || data.nil? 92 | raise I3bangError, "malformed condition #{condition}" unless condition.index '=' 93 | var, val = condition.split '=', 2 94 | if ENV[var] == val 95 | data 96 | else 97 | '' 98 | end 99 | } 100 | 101 | # next handle !@<...> sections 102 | i3bang_sections = Hash.new {|_, x| 103 | raise I3bangError, "unknown section #{x}" 104 | } 105 | config.gsub!(/!@<+([\s\S]*?\n)>(?=\n)|!@<([^>+][^>]*)>/) { 106 | sec = $1 || $2 107 | if sec.include? "\n" 108 | name, data = sec.split "\n", 2 109 | noecho = false 110 | if name[0] == '*' 111 | noecho = true 112 | name = name[1..-1] 113 | end 114 | i3bang_sections[name] = data 115 | noecho ? '' : data 116 | else 117 | i3bang_sections[sec] 118 | end 119 | } 120 | 121 | # then expand !!<...> into separate lines 122 | exrgx = /!!<([^>]*)>/ 123 | while config =~ exrgx 124 | config.sub!(/^.*#{exrgx}.*$/) {|line| 125 | expansions = line.scan(exrgx).map{|expansion| 126 | group, values = expansion[0].split('!', 2) 127 | if values.nil? 128 | values = group 129 | group = "__default_group" 130 | end 131 | [group, values.gsub(/(\d+)\.\.(\d+)/) { 132 | [*$1.to_i..$2.to_i] * ?, 133 | }.split(?,, -1)] 134 | } 135 | group = expansions[0][0] 136 | # equalize length of values for same groups 137 | maxlen = expansions.select{|g, _| g == group}.map(&:last).map(&:size).max 138 | expansions.map! {|g, values| 139 | g == group ? 140 | [g, (values * (maxlen * 1.0 / values.length).ceil)[0...maxlen]] : 141 | [g, values] 142 | } 143 | Array.new(expansions[0][1].length) { line.clone }.map {|l| 144 | idx = -1 145 | l.gsub(exrgx) {|m| 146 | idx += 1 147 | expansions[idx][0] == group ? expansions[idx][1].shift : m 148 | } 149 | }.join "\n" 150 | } 151 | expand_nobracket config if nobracket 152 | end 153 | 154 | # line continuations again (maybe the expansion created more) 155 | config.gsub! /\\\n\s*/, '' 156 | 157 | # now replace all variables/math (!<...>) with their eval'd format 158 | i3bang_vars = Hash.new {|_, k| 159 | if k.is_a? Symbol 160 | raise I3bangError, "unknown variable #{k}" 161 | else 162 | k 163 | end 164 | } 165 | config.gsub!(/(?]*)>/) { 166 | s = $1 167 | # Now we i3bang_eval s 168 | # http://en.wikipedia.org/wiki/Shunting-yard_algorithm 169 | # we assume everything is left-associative for simplicity 170 | 171 | # precedence and stacks setup 172 | prec = Hash.new {|_, x| 173 | if x.nil? || '()'.include?(x) 174 | -1 175 | else 176 | raise I3bangError, "unknown operator #{x}" 177 | end 178 | }.merge({ 179 | '=' => 0, 180 | '+' => 1, '-' => 1, 181 | '*' => 2, '/' => 2, '%' => 2, 182 | '**' => 3 183 | }) 184 | rpn = [] 185 | opstack = [] 186 | 187 | # tokenize input 188 | tokens = s.gsub(/\s/, '').scan(/ 189 | \w[\w\d]* | # variable 190 | \d+(?:\.\d+)? | # number literal 191 | \*\* | # multi-character operator 192 | . # other operator 193 | /x) 194 | 195 | # Shunting-yard 196 | op = nil 197 | tokens.each do |t| 198 | case t[0] 199 | when 'a'..'z', 'A'..'Z', '_' # variable 200 | rpn.push t.to_sym 201 | when '0'..'9' # number literal 202 | rpn.push t.to_i 203 | when '(' # open paren 204 | opstack.push t 205 | when ')' # close paren 206 | while (op = opstack.pop) != '(' 207 | raise I3bangError, 'mismatched parens' if op.nil? 208 | rpn.push op 209 | end 210 | else # operator 211 | rpn.push opstack.pop while prec[t] <= prec[opstack[-1]] 212 | opstack.push t 213 | end 214 | end 215 | rpn.push op while (op = opstack.pop) 216 | 217 | # evaluate rpn 218 | stack = [] 219 | rpn.each do |t| 220 | case t 221 | when Fixnum, Symbol 222 | stack.push t 223 | when '=' 224 | b, a = stack.pop, stack.pop 225 | raise I3bangError, "not enough operands for #{t}" if a.nil? || b.nil? 226 | i3bang_vars[a] = i3bang_vars[b] 227 | else 228 | b, a = stack.pop, stack.pop 229 | raise I3bangError, "not enough operands for #{t}" if a.nil? || b.nil? 230 | stack.push (case t 231 | when '+' then ->a, b { a + b } 232 | when '-' then ->a, b { a - b } 233 | when '*' then ->a, b { a * b } 234 | when '/' then ->a, b { a / b } 235 | when '%' then ->a, b { a % b } 236 | when '**' then ->a, b { a ** b } 237 | when '(' then raise I3bangError, 'mismatched parens' 238 | end)[i3bang_vars[a], i3bang_vars[b]] 239 | end 240 | end 241 | 242 | i3bang_vars[stack[0]] 243 | } 244 | 245 | # cleanup: remove empty lines 246 | config.gsub! /\n+/, "\n" 247 | config.sub! /\A\n/, '' 248 | 249 | config 250 | 251 | end 252 | 253 | if __FILE__ == $0 254 | INFILE = File.expand_path(ARGV[0] || '~/.i3/_config') 255 | OUTFILE = File.expand_path(ARGV[1] || '~/.i3/config') 256 | 257 | config = File.read INFILE 258 | begin 259 | File.write(OUTFILE, i3bang(config, " 260 | ########## 261 | # Generated via i3bang (https://github.com/KeyboardFire/i3bang). 262 | # Original file: #{ARGV[0] || '~/.i3/_config'} 263 | ##########\n")) 264 | rescue I3bangError => e 265 | File.write('/tmp/i3bangerr.txt', "#{e.inspect}\n#{e.backtrace * "\n"}") 266 | `i3-nagbar -m \ 267 | 'There was an error parsing your config file with i3bang!' \ 268 | -b 'show errors' 'i3-sensible-pager /tmp/i3bangerr.txt'` 269 | end 270 | end 271 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require_relative 'i3bang.rb' 4 | require 'test/unit' 5 | 6 | module Test::Unit::Assertions 7 | def assert_i3bang i3bang_in, i3bang_out, *a 8 | assert_equal i3bang("#!nobracket\n#{i3bang_in}").rstrip, i3bang_out, *a 9 | end 10 | 11 | def assert_i3bang_raise i3bang_in, *a 12 | assert_raise(I3bangError, *a) { 13 | i3bang "#!nobracket\n#{i3bang_in}" 14 | } 15 | end 16 | end 17 | 18 | class TestI3bang < Test::Unit::TestCase 19 | # a bit deceiving of a name; not all of these are strict identity 20 | def test_identity 21 | assert_i3bang 'test', 'test' 22 | assert_i3bang "foo\nbar\nbaz", "foo\nbar\nbaz" 23 | assert_i3bang "foo#!<1+>!!\nbar", "foo\nbar" 24 | assert_i3bang "foo \nbar\t \t ", "foo\nbar" 25 | end 26 | 27 | def test_variables 28 | assert_i3bang '!!a', '42' 29 | assert_i3bang '!!!!', '12' 30 | assert_i3bang "!n=1337\n!n", '1337' 31 | end 32 | 33 | def test_math_operations 34 | assert_i3bang '!22+20', '42' 35 | assert_i3bang '!6*9', '54' # nice try, we're not in base 13 36 | assert_i3bang '!200-13', '187' 37 | assert_i3bang '!8/3', '2' # int division 38 | assert_i3bang '!8%3', '2' 39 | assert_i3bang '!2**8', '256' 40 | end 41 | 42 | def test_math_precedence 43 | assert_i3bang '!5+6*7', '47' 44 | assert_i3bang '!(5+6)*7', '77' 45 | assert_i3bang '!!a', '7' 46 | end 47 | 48 | def test_math_errors 49 | assert_i3bang_raise '!thisdoesntexist' 50 | assert_i3bang_raise '!2$2' 51 | assert_i3bang_raise '!((1+1)' 52 | assert_i3bang_raise '!(1+1))' 53 | assert_i3bang_raise '!1+' 54 | end 55 | 56 | def test_basic_expansions 57 | assert_i3bang 'b!!t', "bat\nbot\nbit\nbut" 58 | assert_i3bang 'e!!at,xterminate,xamine potatoes', 59 | "eat potatoes\nexterminate potatoes\nexamine potatoes" 60 | assert_i3bang 'pi!!e,ll,e,e,', "pie\npill\npie\npie\npi" 61 | end 62 | 63 | def test_range_expansions 64 | assert_i3bang 'test !!8..11', "test 8\ntest 9\ntest 10\ntest 11" 65 | assert_i3bang 'fizz!!1..4,buzz,6..7', 66 | "fizz1\nfizz2\nfizz3\nfizz4\nfizzbuzz\nfizz6\nfizz7" 67 | assert_i3bang '!!1,3..5,8,10..13', "1\n3\n4\n5\n8\n10\n11\n12\n13" 68 | end 69 | 70 | def test_dual_expansions 71 | assert_i3bang 'bind !!1..9,0 workspace !!1..10', 72 | "bind 1 workspace 1\n" + "bind 2 workspace 2\n" + 73 | "bind 3 workspace 3\n" + "bind 4 workspace 4\n" + 74 | "bind 5 workspace 5\n" + "bind 6 workspace 6\n" + 75 | "bind 7 workspace 7\n" + "bind 8 workspace 8\n" + 76 | "bind 9 workspace 9\n" + "bind 0 workspace 10" 77 | assert_i3bang '!!!!1,2,3', "a1\nb2\nc3\nd1\ne2" 78 | assert_i3bang '!!!!foo!4,5,6,7', "a4\nb5\na6\nb7" 79 | assert_i3bang '!!<1,2,3>!!<4,5>!!<6,7,8,9>!!<10>', 80 | "14610\n25710\n34810\n15910" 81 | end 82 | 83 | def test_separate_expansions 84 | assert_i3bang '!!<1!a,b,c>!!2!1,2', "a1\na2\nb1\nb2\nc1\nc2" 85 | assert_i3bang '!!!!x!1,2', "foo1\nfoo2\nbar1\nbar2" 86 | assert_i3bang '!!!!3,4', "i3\ni4\nj3\nj4" 87 | end 88 | 89 | def test_multiple_expansion_groups 90 | assert_i3bang '!!!!A!1,2,3 !!!!B!4,5', 91 | "a1 d4\na1 e5\nb2 d4\nb2 e5\nc3 d4\nc3 e5" 92 | assert_i3bang '!!!!1,2 !!!!x!3,4', 93 | "a1 x3\na1 y4\na1 z3\nb2 x3\nb2 y4\nb2 z3\nc1 x3\nc1 y4\nc1 z3" 94 | assert_i3bang '!!<4,7>!!<2,9,5>!!!!<0,12>', 95 | "420\n42.0\n7912\n79.12\n450\n45.0" 96 | end 97 | 98 | def test_sections 99 | assert_i3bang "!@\n!@foo", 100 | "foo\nbar\nbaz\nfoo\nbar\nbaz" 101 | assert_i3bang "!@<*a\nblah>\nfoo !@a baz", 'foo blah baz' 102 | assert_i3bang "!@<+foo\n!<1+1>\n>\n!@foo", "2\n2" 103 | end 104 | 105 | def test_section_errors 106 | assert_i3bang_raise "!@thisdoesntexist" 107 | end 108 | 109 | def test_conditionals 110 | ENV['foo'] = 'bar' 111 | assert_i3bang "!?", 'baz' 112 | assert_i3bang "!?", '' 113 | end 114 | 115 | def test_conditional_errors 116 | assert_i3bang_raise "!?<\n>" 117 | assert_i3bang_raise "!?" 118 | end 119 | 120 | def test_line_continuations 121 | assert_i3bang "foo\\\nbar", 'foobar' 122 | assert_i3bang "foo \\\n bar", 'foo bar' 123 | assert_i3bang "foo\nbar\\\nbaz\\\nqux\nquux", "foo\nbarbazqux\nquux" 124 | assert_i3bang "foo \\ #comment\nbar", 'foo bar' 125 | end 126 | 127 | def test_bang_parsing 128 | assert_i3bang '! 1 + 1', '2' 129 | assert_i3bang "!@!@", '!!!!!!!!!!' 130 | assert_i3bang '!1+1 + 2 ', '4' 131 | assert_i3bang ' !1+1 + 2 ', '4' 132 | assert_i3bang '_!1+1 + 2 ', '_2 + 2' 133 | end 134 | end 135 | --------------------------------------------------------------------------------