BitWarden(rs)를 라즈베리파이에 설치하기

과정

subdomain ipv6으로 설정

젤 오래걸리니 젤 먼저하는 걸 추천 v4는 A고 v6는 AAAA인것만 기억하면 어려울것 없다.
주소는 ifconfig로 확인가능. (아마 없을테니까 apt로 깔자)

Docker

1
2
3
4
sudo apt install docker docker-compose
sudo adduser bitwarden
sudo groupadd docker
sudo usermod -aG docker bitwarden

Let’s Encrypt

난 여기서 파일 권한 문제로 삽질 많이했는데 그냥 bitwarden계정으로 도커를
실행하면 알아서 퍼미션 잡힐테니 그냥 이렇게 하면 된다.

1
2
3
4
5
6
sudo su - bitwarden
docker run -it --rm --name certbot \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
certbot/certbot:arm64v8-latest certonly \
--standalone -d $YOUR_DOMAIN_HERE -m $YOUR_EMAIL_HERE --agree-tos

서버 띄우기

start.sh파일을 만들어두자.
로그볼일이 있을까 싶어서 일부러 --rm옵션은 주지 않았다.
ROCKET_ADDRESS는 디폴트가 0.0.0.0이라 그대로두면 IPv4에서 밖에 인식 못하니 주의

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
yes | docker container prune # prune이 싫으면 docker rm bitwarden 해도 된다
docker run -d --name bitwarden \
-e ROCKET_TLS="{certs=\"/letsencrypt/live/$YOUR_DOMAIN_HERE/fullchain.pem\",key=\"/letsencrypt/live/$YOUR_DOMAIN_HERE/privkey.pem\"}" \
-e ROCKET_ADDRESS='::' \
-e ADMIN_TOKEN="$ADMON_TOKEN" \
--hostname your.servername.here \
-v /etc/letsencrypt/:/letsencrypt/ \
-v /home/bitwarden/bwdata/:/data/ \
-p 443:80 \
vaultwarden/server:latest

인증서 자동 갱신

renew.sh파일도 만들자.

1
2
3
4
5
6
#!/bin/bash
yes | docker container prune
docker run -it --name certbot \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
certbot/certbot:arm64v8-latest renew

매월 15일에 갱신하고 내친김에 리붓했을때 서버 자동으로 켜게 해두자.

1
2
3
crontab -u bitwarden -e
00 04 15 * * /home/bitwarden/renew.sh
@reboot /home/bitwarden/start.sh

IPv6 IP 고정

라즈비안에서는 그런 일없었는데 리붓할때 마다 아이피가 바뀐다.

Network -> Wired -> Config -> IPv6가서 다음 과 같이 설정한다.
v6 IP는 대충 2409:10:12:34:56:78:90:12라고 가정해보자.

Method: Manual
Address: 2409:10:12:34:56:78:90:12
Prefix: 2409:10:12:34::/64
Gateway: 2409:10:12:34::1

리붓하면 아이피가 고정된다.

Todo

갱신이 7월 16일이니 그때 갱신 됐는지 확인하면 될듯.

이력

  • 2021.05.22 IP 고정항목 추가, bitwardenrs -> vaultwarden, ROCKET_ADDRESS 추가

brew에 기여하기

동기

굉장히 드물게 homebrew에 최신버전이 없는걸 내가 젤 먼저 눈치챌 때가 있다.
새버전 나올때 까지 기다리는 것도 방법이긴 한데 의외로 기여하는 방법이 어렵지
않으니 어떻게 해야하는지 알고 있다면 직접 할 수도 있다.

덧붙이자면 이런 류의 기여는 재미있어서 한다기 보다는 내가 시간이 있고 마침
손이 비니까 하는 환경 미화 같은 것이다.

발견

내 경우는 배포처에서 새버전을 만들면서 기존 버전을 지워서 눈치챌 수 있었다.

1
2
3
4
5
❯ brew install texshop
==> Downloading https://pages.uoregon.edu/koch/texshop/texshop-64/texshop458.zip
-#O#- # #
curl: (22) The requested URL returned error: 404 Not Found
Error: Download failed on Cask 'texshop' with message: Download failed: https://pages.uoregon.edu/koch/texshop/texshop-64/texshop458.zip

한일

먼저 edit로 해당 탭을 열어 버전만 수정해서 인스톨 해본다.

1
2
3
4
5
6
7
8
9
10
❯ brew edit texshop
Editing /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask/Casks/texshop.rb
❯ brew install texshop
==> Downloading https://pages.uoregon.edu/koch/texshop/texshop-64/texshop459.zip
######################################################################## 100.0%
Error: SHA256 mismatch
Expected: 97ccf1ebfbb30b8557c972ee2a207a4b570057ae127b9b93cfada73ffbdc907e
Actual: 5c018481244098c0d622f9b976cd0af1f923c7a9e80ab6fe3969ad4b338334fa
File: /Users/marocchino/Library/Caches/Homebrew/downloads/03b3398e2bc593405fc7eeaa245c15cf0e9cfbf0a8882b5d0e6d7e2ac9b08c5e--texshop459.zip
To retry an incomplete download, remove the file above.

sha가 다르다고 통과시켜주지 않으니 저것도 고쳐주자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
~ took 1m41s ❯ brew edit texshop
Editing /usr/local/Homebrew/Library/Taps/homebrew/homebrew-cask/Casks/texshop.rb
~ took 10s ❯ brew install texshop
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 2 taps (homebrew/core and homebrew/cask).
==> Updated Formulae
Updated 1 formula.
==> Updated Casks
Updated 2 casks.

==> Downloading https://pages.uoregon.edu/koch/texshop/texshop-64/texshop459.zip
Already downloaded: /Users/marocchino/Library/Caches/Homebrew/downloads/03b3398e2bc593405fc7eeaa245c15cf0e9cfbf0a8882b5d0e6d7e2ac9b08c5e--texshop459.zip
==> Installing Cask texshop
==> Moving App 'TeXShop.app' to '/Applications/TeXShop.app'
🍺 texshop was successfully installed!

설치 해서 문제없이 프로그램이 열리는 걸 확인했으면 이제 방금 수정한 내용으로
PR을 던진다.

탬플릿을 볼 수 있는데 밑에 새캐스크 만드는 법을 무시하면 내가해야할 일을 3가지
이다.

Important: Do not tick a checkbox if you haven’t performed its action.
Honesty is indispensable for a smooth review process.

After making all changes to a cask, verify:

시키는 대로 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
❯ brew audit --cask texshop
==> Installing 'bundler' gem
... 생략 ...
Bundle complete! 30 Gemfile dependencies, 82 gems now installed.
Bundled gems are installed into `../../usr/local/Homebrew/Library/Homebrew/vendor/bundle`
Post-install message from sorbet:

Thanks for installing Sorbet! To use it in your project, first run:

bundle exec srb init

which will get your project ready to use with Sorbet.
After that whenever you want to typecheck your code, run:

bundle exec srb tc

For more docs see: https://sorbet.org/docs/adopting
audit for texshop: passed
❯ brew style --fix texshop

1 file inspected, no offenses detected

CI가 돌고 문제가 없으면 바로 머지된다. (8분 밖에 걸리지 않았다!)

Blog Recovery

오랬동안 방치했더니 죽어있길레 짬짬히 시간내서 살림.

한 작업들은 다음과 같다.

  • A record 변경: 예전에 설정했던 값이랑 바뀌어서 갱신 해줌 참고링크
    • 적용하고 반영되는데 반나절이상 걸려서 귀찮았다.
  • publish 브랜치를 master에서 main으로 변경
  • 레포이름을 marocchino.github.com -> marocchino.net으로 변경
    • 이참에 dev도메인도 살까 싶음
  • 테마의 대상 레포 변경
    • hexo 테마를 포크해서 폰트만 바꿔서쓰고 있었는데, 시큐리티 업뎃이 귀찮아서
      대량으로 레포지우다 포크떠논 레포도 실수로 지워버렸다가 복구하면서 그냥
      업스트림 바라보게 변경
  • hexo 메어져 업데이트
    • 그냥 워닝만 고쳐주니 돌아감
  • markdownlintrc -> markdownlint.json
    • 이것도 메이져 업데이트하면서 설정 파일이 바뀐듯

살렸으니 당분간 이것저것 삽질해봐야지

id를 사용하는 컬럼을 저장하려면

에초에 이런 모델이있었다.

1
2
class Product < ApplicationRecord
end

url에 아이디 대신 상품 코드를 표시하고 싶다는 요구사항이 추가 되었다. 뭐 이정도야 껌이지.

1
2
3
4
5
6
7
8
9
class Product < ApplicationRecord
def self.find_by_code(code)
find(code.sub(/\AC0+/, ''))
end

def code
"C#{id.to_s.rjust(7, '0')}"
end
end

코드를 커스터마이징 하고싶다는 요구사항이 추가되었다. DB에 저장해야하는데 before_create 시점에서는 모델의 아이디를 뽑을 수 없다. 저장을 두번 하는게 신경쓰이긴하지만 어쩔수 없지 after_save 인가.

1
2
3
4
5
6
7
8
9
10
class Product < ApplicationRecord
after_create :generate_code

private

def generate_code
self.code ||= "C#{id.to_s.rjust(7, '0')}"
throw(:abort) unless save
end
end

테스트를 돌려보면 워닝이 대량 발생한다.

1
2
3
4
5
DEPRECATION WARNING: The behavior of attribute_changed? inside of after...
.DEPRECATION WARNING: The behavior of attribute_changed? inside of after...
.DEPRECATION WARNING: The behavior of attribute_changed? inside of after...
.DEPRECATION WARNING: The behavior of attribute_changed? inside of after...
...

으으.. 이건 save안에서 업데이트할 컬럼을 찾기위해 attribute_changed?를 부르기 때문이다. 성능에도 안좋고 좀 진지하게 검색해서 before_create로 옮기는 대신 아이디를 직접뽑는 방향으로 전환.

1
2
3
4
5
6
7
8
9
10
11
12
class Product < ApplicationRecord
before_create :generate_id_and_code

private

def generate_id_and_code
self.id ||=
self.class.connection
.select_value("select nextval('#{self.class.sequence_name}')")
self.code = "C#{id.to_s.rjust(7, '0')}" unless code?
end
end

이제 저장도 한번하고 테스트돌려도 워닝안나온다. 만족

설계와 테스트

도입

tl;dr red - green - refactor

어디 보낼글 아니니 반말로 써야지.

테스트 안하는 사람들의 논리는 몇가지 패턴이 있다.

  • 바빠서 못해요.
  • 나는 테스트에 가치를 발견하지 못하겠다.
  • 테스트할 만한 가치가 없는 부분이에요.
  • 변경이 너무 잦아서 의미가 없어요.

위에 있는 경우는 뭐 설득당할 생각이 없거나 여유가 없는거라 어쩔 수 없는데 오늘
설득 가능할 것 같은 새 패턴을 발견해서 여기에 대한 이야기를 해볼까한다.

  • 테스트하려고 설계를 망치긴 싫어요.

읭??

좋은 설계라구?

테스트충으로써 tdd를 하면 많은 메서드와 비대한 객체로 끌려가는 건 안다. 다만
그걸 리펙터링할 타이밍을 못잡는 상황이 문제인거지 이게 tdd의 문제라 생각하지는
않는다. 오히려 중요한 부분인데 설계 문제로 테스트를 못한다면 난 설계에 대해
다시 고민하지 테스트를 줄여서 좋은 설계를 만들자는 주장은 안할 것 같다.

좋은 테스트가 좋은 설계를 보장하지 않지만 모든 좋은 설계는 좋은 테스트를 가지고 있다

가령 지금 테스트를 줄여 이쁘게 코드가 나왔다고 해도 코드는 고정된 자산이 아니다.
많약 그런식으로 고정할 수 있는 코드라면 그냥 한번 잘짜면 되지 에초에 테스트도
필요없다.

그래서 테스트 팁

뭐든 많이해보고 익숙해 지는 게 최고다. 굳 올드 red-green-refactor만큼 좋은 bp를
본적이 없으니 그거 그냥 하자능.

1. red

아는 범위에서 최대한 신중하게 최소 스팩을 정한다. 설계를 꼼꼼히 하고 싶다면 이
단계에서 해라. 단위 테스트를 먼저하느라 설계가 이상해진다면 통합 스팩을 먼저
작성해라. bdd를 한다면 문장을 만들어서 기획자한테 내가 이해한게 니가 기획한거랑
일치한지 확인하는게 좋다. 근데 생각해보면 기획자의 경험이나 이해도는 내가 어떻게
할 수 있는 게 아니긴하다.

2. green

아는 범위에서 최대한 구현한다. 모르는 건 어쩔 수 없다.

3. refactor

모든 코드가 적절한지 고민하고 필요없는 부분은 늦기전에 지운다. 딱히 정답이 있는
부분도 아니고 경험이 잘 설명될 수 있는 부분도 아니고 이게 젤 힘들다.
이게 잘 안되서 설계가 어쩌느니 테스트가 어쩌느니 궁시렁 되는데 나한테 권한이
있고 이유를 설명할 수 있다면 모든 코드는 수정 가능하다는 마인드 셋이 가장
중요하고 그 다음은 대량의 코드 수정을 어느정도의 효율로 할 수 있는가 정도
아닐까. 대부분 리뷰에서 수정사항 대량으로 나오면 힘들어하는게 저쪽이라…

그밖에

  • OOP 한정이지만 프라이빗은 테스트하는 거 아님.
    프라이빗이 테스트하고 싶어진다면, 객체를 나눌때가 된게 아닌가 고민해 보라능.
  • 테스트할 때 의존관계 너무 많이 만드는 거 아님. 인생은 테스트 기다리며 지내기엔
    너무 짧다.
  • 너무 완벽하려 노력하지 마라. 에초에 내 실력으로는 택도 없다. 할 수 있는
    범위에서 최대한 하는거지 뭐…
  • 깔끔하게 개선되었을 때의 기분좋은 감각을 기억하라능.
  • 일하는게 너무 헬이라 생각되면 불평하면서 계속다니지 말고 깔끔하게 이직하라능.
    자는 시간 빼고 80% 정도를 일하면서 지내는데 즐겁기라도 해야지.
  • 개발자끼리 의견 일치를 못볼때 설득하는 거보다 짜서 보여주는게 효율적임.
    내가 많이 당했는데 별로 맘에 안들어도 완성품 들고오면 리젝하기 힘들더라.

묵시는 사악해

이 글은 루비 대림 달력에 빈칸 매꾸려고 쓴 글입니다.

다음 코드의 결과를 예측해보세요.

1
2
3
4
5
6
7
8
9
a = //
# 1
a == //

# 2
a.! == //.!

# 3
!a == !//

루비 2.4 기준으로 정답은 1. true, 2. true, 3. false 입니다.

해설

루비에서 falsey값은 아시다시피 nilfalse뿐입니다. //같은 정규식 객체가 falsey로 취급되는건 누가봐도 이상하죠. 이것 저것 시도하다 if문안에 넣어보니 단서가 될만한 경고를 찾았습니다.

1
2
3
irb> p 1if //
(irb):2: warning: regex literal in condition
=> nil

스택오버플로우에 따르면 정규식 리터럴은 묵시적으로 Regexp#~로 해석되고 이는 $_에 대해 매칭을 수행하고 결과를 반환합니다. 결국 !//! ~//로 변환되고 ~//$_에 들어있는 nil에 매칭해 nil을 반환 !niltrue로 변환되게 됩니다. $_는 변수라 여기에 값을 넣으면 행동이 바뀝니다.

1
2
3
4
5
6
7
8
+ irb(main):004:0* $_ = "하이요"
=> "하이요"
+ irb(main):005:0> //
=> //
+ irb(main):006:0> ~//
=> 0
+ irb(main):007:0> !//
=> false

결론

어쨋든 이런 묵시적 변환은 가독성을 해칩니다. 이기회에 좀더 현대적인 언어로 갈아 타던가 파서가 해석하는 대로 적어서 오해를 줄이도록 합시다.

1
2
3
4
5
6
!a != ! ~//

1a != //.match?($_)

text = gets
1a != //.match?(text)

rspec 잘쓰고 계신가요?

이 글은 루비 대림 달력용으로 작성하는 글 입니다.

tl;dr: Given, When, Then 단위로 나누어서 작성하세요.

rspec은

설명할 것도 없이 레일스를 사용한다면 한번은 봤을 법한 가장 대중적인 BDD테스트 프레임 워크입니다.

BDD는 뭔가요

사양을 기술에 집중하는 TDD의 확장입니다. 루비에서는 rspec말고도 minitest-spec, cucumber, rspec-feature등을 사용해 할 수도 있습니다. 특정 서브젝트에 대해 조건을 주고 그 결과를 확인하는 3단계로 나누어 작성하는게 특징입니다. 에러를 읽기도 쉽고 찾기도 쉽죠.

코드로 이야기 합시다

다음과 같은 사양서의 테스트를 작성해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
기능: Stack

조건 새 스택을 만듬
그러면 비어있음

만일 스택에 요소가 추가됨
그러면 그 요소가 스택의 제일 위에 위치함

만일 스택이 N개의 요소를 가짐
그리고 요소 E가 스택의 제일 위에 위치함
그러면 팝 연산은 E를 반환함
그리고 새 스택 크기는 N-1이 됨

먼저 minitest로 작성해 보면 이렇게 될 것 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class StackTest < Minitest::Test
def setup
@stack = Stack.new
end

def test_new
assert @stack.empty?
end

def test_add
@stack.add("A")
assert_equal "A", @stack.top
end

def test_pop
elements = %w(A B C D E)
elements.each { |e| @stack.add(e) }
assert_equal "E", @stack.pop
assert_equal 4, @stack.size
end
end

일단 있는 그대로 rspec문법으로 옮겨보죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RSpec.describe Stack do
subject(:stack) { Stack.new }

it ".new" do
is_expected.to be_empty
end

it "#top" do
stack.add("A")
expect(stack.top).to eq "A"
end

it "#pop" do
elements = %w(A B C D E)
elements.each { |e| stack.add(e) }
expect(stack.pop).to eq "E"
expect(stack.size).to be 4
end
end

TDD일때는 문제가 안되지만, 이런 코드는 설명을 코드의 구현에 의존하기 때문에 설명충이 미덕인 BDD로써는 좋은 코드가 아닙니다. 단계별로 개선해 봅시다.

describe 사용하기

사양서의 Given에 해당하는 부분이고 테스트 할 대상을 지정할 때 사용하는 키워드 입니다. 이미 Stack이 describe 되어있긴 하지만, 관례대로 클래스명 -> 메서드명의 두 단계로 넣어 무엇을 태스트하는지 좀 더 명확하게 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
RSpec.describe Stack do
subject(:stack) { Stack.new }
describe ".new" do
it "비어 있음" do
expect(stack.top).to eq "A"
end
end

describe "#top" do
it "만일 스택에 요소가 추가됨 그러면 그 요소가 스택의 제일 위에 위치함" do
stack.add("A")
expect(stack.top).to eq "A"
end
end

describe "#pop" do
it "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함" \
"그러면 팝 연산은 E를 반환함 그리고 새 스택 크기는 N-1이 됨" do
elements = %w(A B C D E)
elements.each { |e| stack.add(e) }
expect(stack.pop).to eq "E"
expect(stack.size).to be 4
end
end
end

아직 설명이 너무 길군요. 조금 더 분해해 봅시다.

context 사용하기

조건을 정의할 때 사용합니다. 사양의 만일(When)에 해당하는 부분이죠. 중요한 포인트는 context와 before를 하나의 묶음 처럼 생각하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe "#top" do
context "만일 스택에 요소가 추가됨" do
before { stack.add("A") }
it "그러면 그 요소가 스택의 제일 위에 위치함" do
expect(stack.top).to eq "A"
end
end
end

describe "#pop" do
context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함" do
before { %w(A B C D E).each { |e| stack.add(e) } }
it "그러면 팝 연산은 E를 반환함 그리고 새 스택 크기는 N-1이 됨" do
expect(stack.pop).to eq "E"
expect(stack.size).to be 4
end
end
end

부작용 없에기

BDD의 세계에서는 실행 시간을 희생해서라도 부작용을 없에고 싶어합니다. it 하나에 expect하나 이상 사용하는것은 좋지않은 징후죠. 나누어 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
describe "#pop" do
context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함" do
before { %w(A B C D E).each { |e| stack.add(e) } }
it "그러면 팝 연산은 E를 반환함" do
expect(stack.pop).to eq "E"
end

it "그러면 팝연산 후의 새 스택 크기는 N-1이 됨" do
stack.pop
expect(stack.size).to be 4
end
end
end

이제 더 할일이 없어보이네요.

일단 완성

전체 코드는 이렇습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
RSpec.describe Stack do
subject(:stack) { Stack.new }
describe ".new" do
it "비어 있음" do
expect(stack.top).to eq "A"
end
end

describe "#top" do
context "만일 스택에 요소가 추가됨" do
before { stack.add("A") }
it "그러면 그 요소가 스택의 제일 위에 위치함" do
expect(stack.top).to eq "A"
end
end
end

describe "#pop" do
context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함" do
before { %w(A B C D E).each { |e| stack.add(e) } }
it "그러면 팝 연산은 E를 반환함" do
expect(stack.pop).to eq "E"
end

it "그러면 팝연산 후의 새 스택 크기는 N-1이 됨" do
stack.pop
expect(stack.size).to be 4
end
end
end
end

보시는 것 처럼 TDD 스타일에 비해 자연어에 가깝게 적으려는 노오오력이 많이 필요합니다. 하지만 내부 코드를 몰라도 단계적으로 조건의 설명이 명확히 되는건 장점이라 할 수 있죠. 저는 이정도로 만족합니다만, 좀 더 bdd사양에 가깝게 작성하시고 싶으시면 rspec-given이라는 dsl이 있긴 합니다.

rspec-given

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
RSpec.describe Stack do
Given(:stack) { Stack.new }
describe ".new" do
Then "비어 있음" do
expect(stack.top).to eq "A"
end
end

describe "#top" do
context "만일 스택에 요소가 추가됨" do
When { stack.add("A") }
Then "그러면 그 요소가 스택의 제일 위에 위치함" do
expect(stack.top).to eq "A"
end
end
end

describe "#pop" do
context "만일 스택이 N개의 요소를 가짐 그리고 요소 E가 스택의 제일 위에 위치함" do
When { %w(A B C D E).each { |e| stack.add(e) } }
Then "그러면 팝 연산은 E를 반환함" do
expect(stack.pop).to eq "E"
end

Then "그러면 팝연산 후의 새 스택 크기는 N-1이 됨" do
stack.pop
expect(stack.size).to be 4
end
end
end
end

큐컴버 만큼은 아니지만, 어느정도 정돈되어 보이네요.

결론

bdd에 충실하게 rspec을 작성하는 법을 알아 보았습니다. 일정이 바쁘다던가 사양이 복잡하던가 이러저러한 이유가 있긴하겠지만 DHH도 불평 못하게 깔끔하게 작성하도록 노력해봅시다. :)

참고

Custom matcher in exunit

동기

그냥 assert Regex.match?(regex, actual)는 true/false만 반환해서 디버깅할 때 곤란했는데, 이걸 매번 메세지로 찍어주자니 귀찮아서 아예 매쳐로 만들어 볼려고 하니 문서로 설명되어있지 않아서 정리겸..

준비

피닉스는 이미 설정 되어있어서 필요없지만 직접 만든 라이브러리라면, 루비에서처럼 require로는 못하고 mix.exs에서 로드 경로를 설정해야 한다. 테스트 환경에서만 로드하게 할려면 이렇게 하면된다.

1
2
3
4
5
6
7
8
def project do
[...,
elixirc_paths: elixirc_paths(Mix.env),
...]
end

defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

설정후에 일단 한번 컴파일 해주자.

1
mix compile

매쳐 작성

내가 필요했던건 정규식으로 스트링을 검증해주는 매쳐다. test/support 안에 넣어준다. 파일명이 exs가 아니라 ex인것에 주의.

1
2
3
4
5
6
7
defmodule ProjectName.Matcher do
import ExUnit.Assertions, only: [assert: 2]

def assert_match(regex, actual) do
assert Regex.match?(regex, actual), "#{actual} is not match."
end
end

사용

평범하게 임포트해서 사용하면 된다.

1
2
3
4
import ProjectName.Matcher, only: [assert_match: 2]
test "123 matches \\d+" do
assert_match ~r/\A\d+\z/, "123"
end

끝.

elm from elixir

이 글은 뽐뿌 글이 아니니 엘름의 좋은 점을 알고싶다면 다른 글을 읽으세요.

연산자

조금 다르긴한데 많이 신경쓰이는 정도는 아니다. 몇가지 주목할 부분은..

  • !=대신 \=을 사용한다.
  • and or가 없다.
  • || && 도 Bool 타입만 받아서 엘릭서에서 인라인 if 대신 사용하던 valid or raise Error 를 사용할 수 없다.
  • / 는 Float를 반환한다.
  • div/2//rem/2%로 사용할 수 있다.

함수

basic 문서에 파이프가 없어서 없구나 싶었는데 그냥 메서드에 있었다 . 엘릭서에는 없던 파이프가 몇개 더 있다. 커링에 더 특화되어 있는 느낌.

강타입과 컴파일

아.. 정말 여태까지 이렇게 채크해주는 언어를 해본적이 없어서 정말 미쳐버리는 줄 알았는데 어느 정도 익숙해졌다. 일단 함수 선언 위에 타입을 전부 적어야 하고 이게 실제 리턴 값과 다르면 컴파일이 안된다. 처음에는 타입 맞추느라 삽질많이 했는데 익숙 해지니 그냥 저냥 할 만 하다. 루비나 엘릭서에서는 파이프연결해 중간값 확인하면서 코딩했었는데 최종 타입 맞지않으면 실행이 안되니 그런식으로 코딩하기 힘들어졌다. 일단 제일 많이 삽질했던 두 개만 언급하고 넘어가자.

Maybe XX

그냥 해당 타입이거나 Nothing을 반환 하는데 성공 했을때 (Just XX) 실패 했을때 (Nothing)으로 나온다 map같은 이터레이터에 먹일 때 보통 이런 상태 말고 값만 있을거라 예상하는데, 그렇지 않으니 처리하기 좀 번거롭다. 난 보통 케이스로 처리한다.

1
2
3
case a of
Just x -> x
Nothing -> Debug.crash("error")

뭐.. 당연한 이야기지만 결과가 nil이 아니므로

1
value || default

같은 식으로는 쓸 수 없다. 이렇게 해야한다.

1
2
Maybe.withDefault 100 (Just 42)   -- 42
Maybe.withDefault 100 Nothing -- 100

이터레이터 안에서 value만 뽑고 싶을 때에는 이렇게 하면된다. (내가 할 줄 몰라서) 좀 괴로웠다.

1
2
3
4
5
modifyList : List -> Maybe Nothing (List a)
modifyList list =
list
|> some_maybe_methods
|> List.foldr (Maybe.map2 (::)) (Just [])

Result

성공했을 때 (Ok value), 실패했을 때 (Err reason)이 나온다. 이녀석도 케이스로 처리해야 한다. 그거말고는 Maybe랑 비슷하게 하면 된다.

1
2
3
case a of
Just x -> x
Nothing -> Debug.crash("error")

파이프 순서

엘릭서와 다르게 제일 뒤에 있는게 사용되는데, 덕분에 가독성에서 좀 손해를 보는 느낌이다. 예를 들자면 적어도 나에게는

1
String.contains? "elixir of life", "of"

보다는

1
String.contains "of" "elm tree of life"

가 읽기 힘들다.

1
"elm tree of life" |> String.contains "of"

라고 적을 수 있긴하다. 실제로 그렇게 적을지는 또 다른 문제지만,

라고 적으면 나쁜 점만 있는것 같지만.. 사실 커링 때문에 더 편해진 것도 많다. 이런 상황을 생각해보자.

1
2
3
4
5
6
def add(a, b), do: a + b

def add_all(list, num) do
list
|> Enum.map(&add(&1, num))
end

엘릭서에서는 아리티가 일치하지 않으면 외부 참조를 넘길수 없어서 이런 레핑이 최선이었다.

하지만 엘름은 다르다.

1
2
3
4
5
6
7
8
9
10
11
add : Int -> Int -> Int
add a b =
a + b

addAll : Int -> List a -> List a
addAll num list =
let
plusNum = (add num)
in
list
|> List.map plusNum

인자가 불완전하게 넘겨진 상태도 함수이므로 그걸 그대로 함수 인자로 넘길 수 있다.

패턴 매칭

모든 조건을 커버 하지 못하면 컴파일이 안된다. 특히 리스트가 컴파일 시점에서 갯수까지는 알수 없기때문에 let에는 디스트럭쳐링 할 수 없고 case를 사용해야 한다.

1
[a, b, c] = list |> Enum.sort

그래서 위에 있는 엘릭서에서는 간단히 되던 코드가 이렇게 되버렸다.

1
2
3
case (List.sort list) of
a::b::c::[] -> ...
_ -> Debug.crash("Impossible")

with도 없고 리스트 몇개만 매칭으로 풀어도 case중첩으로 산으로 가서 타입에따라서는 그냥 없다고 생각하고 작성하는게 편할 수도 있다. 패턴 매칭이 약해서 if else 구문을 평범하게 사용하는것도 다른 문화.

기타

  • List의 렌덤 억세스에 관한 함수가 없다. 굳이 하고 싶으면 어레이로 변환해서 해야한다. 근데 변환 여러번 하는 거보다 head tail로 어찌어찌하는게 비용이 적을 것 같다.
  • 요즘은 어떨지 모르겠는데 옛날에 자바 싫어하는 이유 중 하나가 정규식 쓰기 힘들어서 였는데 , 정규식용 리터럴이 따로 준비되어있지 않고 스트링으로 해서 자바에서 정규식 쓰던 생각난다.

결론

단순하게 되던게 번거로워 진 부분이 좀 많아서 생산성만 좀 더 뽑을 수 있으면 좋을 것 같다는 생각을 했다.

Bash tips

안적어두면 자꾸까먹어서 정리할 겸..

여러 파일 치환

이런 작업은 vim에서 하는거보다는 밖에서 하는게 빠르고 정규식도 POSIX라 편하다. 오타 수정할 때 쓰면 좋다.

1
perl -pi -e 's/old-string/new-string/g' my-files-*.txt

조금 덧붙여서 커밋된 파일만 순회할 수도 있다.

1
perl -pi -e 's/old-string/new-string/g' $(git ls-tree HEAD --name-only -r)

특정 폴더만 지정하려면 – 쓰면된다.

1
2
perl -pi -e 's/old-string/new-string/g' \
$(git ls-tree HEAD --name-only -r -- ./folder)

clipboard 내용으로 사전열기

번역하고 있을때 유용하게 쓴다. OSX한정이고 bash tip이 아닐지도 모르겠는데 이렇게 한다. alias로 해두면 세손가락 더블탭보다 스트로크가 적다.

1
open -a /Applications/Dictionary.app/ --args $(pbpaste)

패턴에 따라 여러파일 지정하기

디렉토리 구조가 비슷하다거나. 확장자만 조금 다르거나 할 때 사용할 수있다.

1
2
vim {ko,.}/lessions/basic/test.md
vim readme.{ko,en}.md

if statement

기본적으로 이렇다.

1
2
3
4
5
6
#!/bin/bash
if [[ condition ]]; then
something
else
something
fi

구문은 딱히 특별할거 없고, condition만 신경쓰면 된다.

명령 설명
! 표현식 표현식이 참이 아님
-n 문자열 문자열의 길이가 0보다 큼
-z 문자열 문자열의 길이가 0
문자열1 = 문자열2 두문자열이 같음
문자열1 != 문자열2 두문자열이 다름
숫자1 -eq 숫자2 두숫자가 같음
숫자1 -ne 숫자2 두숫자가 다름
숫자1 -gt 숫자2 숫자1이 숫자2보다 큼
숫자1 -lt 숫자2 숫자1이 숫자2보다 작음
-d 파일 디렉토리가 있음
-e 파일 파일이 있음
-r 파일 파일이 있고 읽기권한이 있음
-s 파일 파일이 있고 크기가 0보다 큼
-w 파일 파일이 있고 쓰기권한이 있음
-x 파일 파일이 있고 실행권한이 있음

$변수들

표현 설명
$0 셸스크립트 이름
$1 첫 번째 인자
$2 두 번째 인자
$@ 인자 배열
$# 인자 개수

기본값 설정하기

변수가 할당안됐을 기본값을 지정할 수 있다. if문을 줄이려할 때 유용.

1
2
3
echo Hello, ${NAME-World}! # => Hello, World!
NAME=Alice
echo Hello, ${NAME-World}! # => Hello, Alice!

ref

https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html http://www.tldp.org/LDP/abs/html/parameter-substitution.html https://github.com/jlevy/the-art-of-command-line/blob/master/README-ko.md