├── .gitignore ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin └── imgurr ├── imgurr.gemspec ├── lib ├── imgurr.rb └── imgurr │ ├── color.rb │ ├── command.rb │ ├── imgurAPI.rb │ ├── imgurErrors.rb │ ├── numbers.rb │ ├── platform.rb │ └── storage.rb └── test ├── 1-upload.sh ├── 2-info.sh ├── 3-delete.sh ├── 4-misc.sh ├── cli.sh ├── error.sh ├── files ├── github-logo.png ├── habs.png └── imgurr-old.json ├── roundup └── run /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | Gemfile.lock 15 | 16 | # YARD artifacts 17 | .yardoc 18 | _yardoc 19 | doc/ 20 | 21 | # Rubymine Files 22 | .idea/ 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Christophe Naud-Dulude, https://github.com/Chris911/ 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 | imgurr [![Gem Version](https://badge.fury.io/rb/imgurr.svg)](http://badge.fury.io/rb/imgurr) 2 | ======= 3 | Command line utility for Imgur in Ruby. Imgurr lets you quickly upload images, get info about an image and delete your own images from Imgur. 4 | 5 | ## Install 6 | gem install imgurr 7 | 8 | ## Usage 9 | 10 | ![](http://i.imgur.com/rGoGCNb.png) 11 | #### Examples 12 | $ imgurr capture 13 | Uploading screenshot... 14 | Copied http://i.imgur.com/rGoGCNb.png to clipboard 15 | 16 | $ imgurr upload image.gif 17 | Copied http://i.imgur.com/PLWGJlc.gif to clipboard 18 | 19 | $ imgurr upload image.jpg --markdown 20 | Copied ![Screenshot](http://i.imgur.com/PLWGJlc.gif) to clipboard 21 | 22 | $ imgurr info 2KxrTAK 23 | Image ID : 2KxrTAK 24 | Views : 14717 25 | Bandwidth : 2.296 GiB 26 | Title : None 27 | Desc : None 28 | Animated : false 29 | Width : 960 px 30 | Height : 540 px 31 | Link : http://i.imgur.com/2KxrTAK.jpg 32 | 33 | $ imgurr delete http://i.imgur.com/2KxrTAK.jpg 34 | Successfully deleted image from Imgur 35 | 36 | ./imgurr --help for more. 37 | 38 | ## How it works 39 | Imgurr stores the delete hash generated by Imgur locally when uploading an image using imgurr. Most of the time the delete hash gets lost if you don't have an account and you want to takedown an image you have to contact Imgur support which can take a while. With imgurr all your delete hashes are saved so you can delete your images later if needed. 40 | 41 | 42 | ## Contributing 43 | 44 | 1. Fork it 45 | 2. Create your feature branch (`git checkout -b my-new-feature`) 46 | 3. Commit your changes (`git commit -am 'Add some feature'`) 47 | 4. Push to the branch (`git push origin my-new-feature`) 48 | 5. Create new Pull Request 49 | 50 | Don't forget to add a test for any new feature. 51 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | 5 | def name 6 | @name ||= Dir['*.gemspec'].first.split('.').first 7 | end 8 | 9 | ############################################################################# 10 | # 11 | # Tests 12 | # 13 | ############################################################################# 14 | 15 | task :default => :test 16 | 17 | desc "Run tests for #{name}" 18 | task :test do 19 | exec "test/run" 20 | end 21 | 22 | desc "Open an irb session preloaded with this library" 23 | task :console do 24 | sh "irb -rubygems -r ./lib/#{name}.rb" 25 | end -------------------------------------------------------------------------------- /bin/imgurr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # coding: utf-8 3 | 4 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 5 | 6 | require 'imgurr' 7 | 8 | Imgurr::Command.execute(*ARGV) -------------------------------------------------------------------------------- /imgurr.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'imgurr' 3 | s.version = '1.0.0' 4 | s.summary = "Imgurr lets you upload images to imgur from the command line" 5 | s.description = "Imgurr is a ruby gem that lets you upload images to Imgur and manage your account" 6 | s.authors = ["Christophe Naud-Dulude"] 7 | s.email = 'christophe.naud.dulude@gmail.com' 8 | s.homepage = 'https://github.com/Chris911/imgurr' 9 | 10 | s.require_paths = %w[lib] 11 | ## If your gem includes any executables, list them here. 12 | s.executables = ["imgurr"] 13 | s.default_executable = 'imgurr' 14 | 15 | s.add_dependency('json') 16 | 17 | s.add_development_dependency('rake', "~> 0.9.2") 18 | 19 | ## Specify any RDoc options here. You'll want to add your README and 20 | ## LICENSE files to the extra_rdoc_files list. 21 | s.rdoc_options = ["--charset=UTF-8"] 22 | s.extra_rdoc_files = %w[README.md LICENSE.md] 23 | 24 | s.license = 'MIT' 25 | 26 | s.files = %w( README.md LICENSE.md Gemfile imgurr.gemspec Rakefile) 27 | s.files += Dir.glob("lib/**/*") 28 | s.files += Dir.glob("bin/**/*") 29 | s.files += Dir.glob("test/**/*") 30 | end 31 | -------------------------------------------------------------------------------- /lib/imgurr.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | begin 4 | require 'rubygems' 5 | rescue LoadError 6 | end 7 | 8 | # External require 9 | require 'net/http' 10 | require 'net/https' 11 | require 'open-uri' 12 | require 'json' 13 | require 'optparse' 14 | 15 | $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 16 | 17 | # Internal require 18 | require 'imgurr/color' 19 | require 'imgurr/numbers' 20 | require 'imgurr/imgurAPI' 21 | require 'imgurr/storage' 22 | require 'imgurr/platform' 23 | require 'imgurr/imgurErrors' 24 | require 'imgurr/command' 25 | 26 | module Imgurr 27 | VERSION = '0.2.0' 28 | DEBUG = false 29 | 30 | def self.storage 31 | @storage ||= Storage.new 32 | end 33 | 34 | def self.options 35 | @options ||= {} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/imgurr/color.rb: -------------------------------------------------------------------------------- 1 | module Imgurr 2 | # Color collects some methods for colorizing terminal output. 3 | # Thanks to https://github.com/holman 4 | module Color 5 | extend self 6 | 7 | CODES = { 8 | :reset => "\e[0m", 9 | 10 | :cyan => "\e[36m", 11 | :magenta => "\e[35m", 12 | :red => "\e[31m", 13 | :yellow => "\e[33m" 14 | } 15 | 16 | # Tries to enable Windows support if on that platform. 17 | # 18 | # Returns nothing. 19 | def self.included(other) 20 | if RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw32/ 21 | require 'Win32/Console/ANSI' 22 | end 23 | rescue LoadError 24 | # Oh well, we tried. 25 | end 26 | 27 | # Wraps the given string in ANSI color codes 28 | # 29 | # string - The String to wrap. 30 | # color_code - The String representing he ANSI color code 31 | # 32 | # Examples 33 | # 34 | # colorize("Boom!", :magenta) 35 | # # => "\e[35mBoom!\e[0m" 36 | # 37 | # Returns the wrapped String unless the the platform is windows and 38 | # does not have Win32::Console, in which case, returns the String. 39 | def colorize(string, color_code) 40 | if !defined?(Win32::Console) && !!(RUBY_PLATFORM =~ /win32/ || RUBY_PLATFORM =~ /mingw32/) 41 | # looks like this person doesn't have Win32::Console and is on windows 42 | # just return the uncolorized string 43 | return string 44 | end 45 | "#{CODES[color_code] || color_code}#{string}#{CODES[:reset]}" 46 | end 47 | 48 | # Set up shortcut methods to all the codes define in CODES. 49 | self.class_eval(CODES.keys.reject {|color| color == :reset }.map do |color| 50 | "def #{color}(string); colorize(string, :#{color}); end" 51 | end.join("\n")) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/imgurr/command.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Command is the main point of entry for boom commands; shell arguments are 4 | # passed through to Command, which then filters and parses through individual 5 | # commands and reroutes them to constituent object classes. 6 | # 7 | # Highly inspired by @holman/boom 8 | # 9 | 10 | module Imgurr 11 | class Command 12 | class << self 13 | 14 | #include Imgurr::Color 15 | 16 | # Public: executes a command. 17 | # 18 | # args - The actual commands to operate on. Can be as few as zero 19 | # arguments or as many as three. 20 | def execute(*args) 21 | command = args.shift 22 | major = args.shift 23 | minor = args.empty? ? nil : args.join(' ') 24 | 25 | return help unless command 26 | parse_options 27 | delegate(command, major, minor) 28 | end 29 | 30 | # Public: Parse extra options 31 | # 32 | # returns nothing 33 | def parse_options 34 | options[:markdown] = false 35 | o = OptionParser.new do |opts| 36 | opts.on('-m', '--markdown', 'Use Markdown Syntax') do 37 | options[:markdown] = true 38 | end 39 | opts.on('-l', '--html', 'Use HTML Syntax') do 40 | options[:html] = true 41 | end 42 | opts.on('-s', '--size PERCENTAGE', 'Image Size') do |value| 43 | options[:size] = value 44 | end 45 | opts.on('-t', '--title TITLE', 'Image Title') do |value| 46 | options[:title] = value 47 | end 48 | opts.on('-d', '--desc DESC', 'Image Title') do |value| 49 | options[:desc] = value 50 | end 51 | opts.on('-v', '--version', 'Print Version') do 52 | version 53 | quit 54 | end 55 | opts.on('-h', '--help', 'Print Help') do 56 | help 57 | quit 58 | end 59 | end 60 | begin 61 | o.parse! 62 | rescue OptionParser::MissingArgument => e 63 | puts "Error: #{e.message}" 64 | quit 65 | rescue OptionParser::InvalidOption => e 66 | puts "Error: #{e.message}" 67 | quit 68 | end 69 | end 70 | 71 | # Public: gets $stdin. 72 | # 73 | # Returns the $stdin object. This method exists to help with easy mocking 74 | # or overriding. 75 | def stdin 76 | $stdin 77 | end 78 | 79 | # Public: accesses the in-memory JSON representation. 80 | # 81 | # Returns a Storage instance. 82 | def storage 83 | Imgurr.storage 84 | end 85 | 86 | # Public: accesses the global options 87 | # 88 | # Returns Options dictionary 89 | def options 90 | Imgurr.options 91 | end 92 | 93 | # Public: allows main access to most commands. 94 | # 95 | # Returns output based on method calls. 96 | def delegate(command, major, minor) 97 | return help unless command 98 | return no_internet unless self.internet_connection? 99 | return capture if command == 'capture' || command == 'cap' 100 | 101 | if major 102 | return upload(major) if command == 'upload' || command == 'up' || command == 'u' 103 | 104 | # Get image ID from URL 105 | if major =~ /.*imgur\.com\/[a-zA-Z0-9]*\.[a-zA-Z]*/ 106 | major = /com\/[a-zA-Z0-9]*/.match(major).to_s.gsub('com/','') 107 | end 108 | 109 | return unless valid_id major 110 | return info(major) if command == 'info' || command == 'i' 111 | return delete(major,minor) if command == 'delete' || command == 'd' 112 | else 113 | return list if command == 'list' || command == 'l' 114 | 115 | puts "Argument required for commmand #{command}." 116 | puts "imgurr --help for more information." 117 | return 118 | end 119 | end 120 | 121 | # Public: Upload an image to Imgur 122 | # 123 | # Returns nothing 124 | def upload(major) 125 | unless File.exist?(major) 126 | puts "File #{major} not found." 127 | return 128 | end 129 | major = File.absolute_path(major) 130 | response, success = ImgurAPI.upload(major) 131 | puts response unless success 132 | if success 133 | response = "![#{options[:title].nil? ? 'Screenshot' : options[:title]}](#{response})" if options[:markdown] 134 | response = build_HTML_response(response) if options[:html] 135 | puts "Copied #{Platform.copy(response)} to clipboard" 136 | end 137 | storage.save 138 | end 139 | 140 | # Public: Capture and image and upload to imgur 141 | # 142 | # Note: Only supported on OS X for now 143 | # Returns nothing 144 | def capture 145 | unless Platform.darwin? 146 | puts "Capture command is only supported on OS X for the time being." 147 | return 148 | end 149 | 150 | image_path = "#{ENV['HOME']}/.imgurr.temp.png" 151 | Platform.capture('-W', image_path) 152 | 153 | # User might have canceled or it takes some time to write to disk. 154 | # Check up to 3 times with 1 sec delay 155 | 3.times do 156 | if File.exist?(image_path) 157 | puts "Uploading screenshot..." 158 | upload(image_path) 159 | File.delete(image_path) 160 | break 161 | end 162 | sleep(1) 163 | end 164 | end 165 | 166 | # Public: List uploaded images 167 | # 168 | # Returns nothing 169 | def list 170 | items = storage.items 171 | if items.empty? 172 | puts 'No items in the list.' 173 | return 174 | end 175 | 176 | storage.items.each do |(id, data)| 177 | puts "#{id} #{data[:stamp]} #{data[:source].ljust(48)}" 178 | end 179 | end 180 | 181 | # Public: build HTML image tag response 182 | # 183 | # Returns a properly formatted HTML tag 184 | def build_HTML_response(response) 185 | return "\"#{options[:title].nil?" if options[:size].nil? 186 | 187 | "\"#{options[:title].nil?" 188 | end 189 | 190 | # Public: Get image info 191 | # 192 | # Returns nothing 193 | def info(major) 194 | response = ImgurAPI.get_info(major) 195 | puts response 196 | end 197 | 198 | # Public: Delete image from imgur 199 | # 200 | # Returns nothing 201 | def delete(major,minor) 202 | major = major.to_sym 203 | if minor 204 | delete_hash = minor 205 | else 206 | if storage.hash_exists?(major) 207 | delete_hash = storage.find(major) 208 | else 209 | puts 'Delete hash not found in storage.' 210 | puts 'Use: imgurr delete ' 211 | return 212 | end 213 | end 214 | if ImgurAPI.delete(delete_hash) 215 | puts 'Successfully deleted image from Imgur' 216 | storage.delete(major) 217 | storage.save 218 | else 219 | puts 'Unauthorized Access. Wrong delete hash?' 220 | end 221 | end 222 | 223 | # Public: the version of boom that you're currently running. 224 | # 225 | # Returns a String identifying the version number. 226 | def version 227 | puts "You're running imgurr #{Imgurr::VERSION}." 228 | end 229 | 230 | # Public: Checks is there's an active internet connection 231 | # 232 | # Returns true or false 233 | def internet_connection? 234 | begin 235 | true if open("http://www.google.com/") 236 | rescue 237 | false 238 | end 239 | end 240 | 241 | # Public: No internet error 242 | # 243 | # Returns nothing 244 | def no_internet 245 | puts 'An Internet connection is required to use this command.' 246 | end 247 | 248 | # Public: Quit / Exit program 249 | # 250 | # Returns nothing 251 | def quit 252 | exit(1) 253 | end 254 | 255 | # Public: Validate id (major) 256 | # 257 | # Returns true if valid id 258 | def valid_id(major) 259 | unless major =~ /^[a-zA-Z0-9]*$/ 260 | puts "#{major} is not a valid imgur ID or URL" 261 | return false 262 | end 263 | return true 264 | end 265 | 266 | # Public: prints all the commands of boom. 267 | # 268 | # Returns nothing. 269 | def help 270 | text = ' 271 | - imgurr: help --------------------------------------------------- 272 | 273 | imgurr --help This help text 274 | imgurr --version Print current version 275 | 276 | imgurr upload Upload image and copy link to clipboard 277 | imgurr upload [-m | --markdown] Upload image and copy link to clipboard with markdown syntax 278 | [-l | --html] Upload image and copy link to clipboard with HTML syntax 279 | [--size=SIZE] Set image size ratio 280 | [--tile="Title"] Set image title 281 | [--desc="Desc" ] Set image description 282 | imgurr capture Capture a screenshot and upload it (OS X only) 283 | imgurr list List uploaded images 284 | imgurr info Print image information 285 | imgurr delete Deletes an image from imgur if the deletehash is found locally 286 | imgurr delete Deletes an image from imgur with the provided deletehash 287 | 288 | all other documentation is located at: 289 | https://github.com/Chris911/imgurr 290 | '.gsub(/^ {8}/, '') # strip the first eight spaces of every line 291 | 292 | puts text 293 | end 294 | 295 | end 296 | end 297 | end 298 | -------------------------------------------------------------------------------- /lib/imgurr/imgurAPI.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # Interface for the Imgur API 5 | # 6 | 7 | module Imgurr 8 | class ImgurAPI 9 | class << self 10 | API_URI = URI.parse('https://api.imgur.com') 11 | API_PUBLIC_KEY = 'Client-ID 70ff50b8dfc3a53' 12 | 13 | ENDPOINTS = { 14 | :image => '/3/image/', 15 | :gallery => '/3/gallery/' 16 | } 17 | 18 | # Public: accesses the in-memory JSON representation. 19 | # 20 | # Returns a Storage instance. 21 | def storage 22 | Imgurr.storage 23 | end 24 | 25 | # Public: accesses the global options 26 | # 27 | # Returns Options dictionary 28 | def options 29 | Imgurr.options 30 | end 31 | 32 | # HTTP Client used for API requests 33 | # TODO: Confirm SSL Certificate 34 | def web_client 35 | http = Net::HTTP.new(API_URI.host, API_URI.port) 36 | http.use_ssl = true 37 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 38 | http 39 | end 40 | 41 | # Public: Upload an image 42 | # 43 | # args - The image path for the image to upload 44 | # 45 | def upload(image_path) 46 | params = {:image => File.read(image_path)} 47 | params[:title] = options[:title] unless options[:title].nil? 48 | params[:description] = options[:desc] unless options[:desc].nil? 49 | request = Net::HTTP::Post.new(API_URI.request_uri + ENDPOINTS[:image]) 50 | request.set_form_data(params) 51 | request.add_field('Authorization', API_PUBLIC_KEY) 52 | 53 | response = web_client.request(request) 54 | handle_upload_response(response.body, image_path) 55 | end 56 | 57 | # Public: Get info about an image 58 | # 59 | # args - The image imgur id 60 | # 61 | def get_info(image_id) 62 | request = Net::HTTP::Get.new(API_URI.request_uri + ENDPOINTS[:image] + image_id) 63 | request.add_field('Authorization', API_PUBLIC_KEY) 64 | 65 | response = web_client.request(request) 66 | handle_info_response(response.body) 67 | end 68 | 69 | # Public: Upload an image 70 | # 71 | # args - The image path for the image to upload 72 | # 73 | def delete(delete_hash) 74 | request = Net::HTTP::Delete.new(API_URI.request_uri + ENDPOINTS[:image] + delete_hash) 75 | request.add_field('Authorization', API_PUBLIC_KEY) 76 | 77 | response = web_client.request(request) 78 | handle_delete_response(response.body) 79 | end 80 | 81 | # Public: Handle API Response: Uploaded Image 82 | # 83 | # args - Response data 84 | # 85 | def handle_upload_response(response, source_path) 86 | data = JSON.parse(response) 87 | puts JSON.pretty_unparse(data) if Imgurr::DEBUG 88 | if data['success'] 89 | storage.add_hash(data['data']['id'], data['data']['deletehash'], source_path) 90 | return [data['data']['link'], true] 91 | end 92 | [ImgurErrors.handle_error(response), false] 93 | end 94 | 95 | # Public: Handle API Response: Get image Info 96 | # 97 | # args - Response data 98 | # 99 | def handle_info_response(response) 100 | data = JSON.parse(response) 101 | puts JSON.pretty_unparse(data) if Imgurr::DEBUG 102 | if data['success'] 103 | return " 104 | Image ID : #{data['data']['id']} 105 | Views : #{data['data']['views']} 106 | Bandwidth : #{Numbers.to_human(data['data']['bandwidth'])} 107 | Title : #{data['data']['title'].nil? ? 'None' : data['data']['title']} 108 | Desc : #{data['data']['description'].nil? ? 'None' : data['data']['description']} 109 | Animated : #{data['data']['animated']} 110 | Width : #{data['data']['width']} px 111 | Height : #{data['data']['height']} px 112 | Link : #{data['data']['link']} 113 | ".gsub(/^ {8}/, '') 114 | end 115 | ImgurErrors.handle_error(response) 116 | end 117 | 118 | # Public: Handle API Response: Delete Image 119 | # 120 | # args - Response data 121 | # 122 | def handle_delete_response(response) 123 | data = JSON.parse(response) 124 | puts JSON.pretty_unparse(data) if Imgurr::DEBUG 125 | data['success'] 126 | end 127 | 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/imgurr/imgurErrors.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # Interface for the Imgur API 5 | # 6 | 7 | module Imgurr 8 | class ImgurErrors 9 | class << self 10 | 11 | def handle_error(response) 12 | data = JSON.parse(response) 13 | "Imgur Error: #{data['data']['error']} (#{data['status']})" 14 | end 15 | 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/imgurr/numbers.rb: -------------------------------------------------------------------------------- 1 | module Imgurr 2 | class Numbers 3 | class << self 4 | def to_human(number) 5 | units = %W(B KiB MiB GiB TiB) 6 | 7 | size, unit = units.reduce(number.to_f) do |(fsize, _), utype| 8 | fsize > 512 ? [fsize / 1024, utype] : (break [fsize, utype]) 9 | end 10 | return "#{"%.3f" % size} #{unit}" 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/imgurr/platform.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Platform is a centralized point to shell out platform specific functionality 4 | # like clipboard access or commands to open URLs. 5 | # 6 | # 7 | # Clipboard is a centralized point to shell out to each individual platform's 8 | # clipboard, pasteboard, or whatever they decide to call it. 9 | # 10 | # Source: https://github.com/holman/boom 11 | # 12 | module Imgurr 13 | class Platform 14 | class << self 15 | # Public: tests if currently running on darwin. 16 | # 17 | # Returns true if running on darwin (MacOS X), else false 18 | def darwin? 19 | !!(RUBY_PLATFORM =~ /darwin/) 20 | end 21 | 22 | # Public: tests if currently running on windows. 23 | # 24 | # Apparently Windows RUBY_PLATFORM can be 'win32' or 'mingw32' 25 | # 26 | # Returns true if running on windows (win32/mingw32), else false 27 | def windows? 28 | !!(RUBY_PLATFORM =~ /mswin|mingw/) 29 | end 30 | 31 | # Public: returns the command used to open a file or URL 32 | # for the current platform. 33 | # 34 | # Currently only supports MacOS X and Linux with `xdg-open`. 35 | # 36 | # Returns a String with the bin 37 | def open_command 38 | if darwin? 39 | 'open' 40 | elsif windows? 41 | 'start' 42 | else 43 | 'xdg-open' 44 | end 45 | end 46 | 47 | # Public: opens a given URL in the browser. This 48 | # method is designed to handle multiple platforms. 49 | # 50 | # Returns nothing 51 | def open(url) 52 | unless windows? 53 | system("#{open_command} '#{url.gsub("\'","'\\\\''")}'") 54 | else 55 | system("#{open_command} #{url.gsub("\'","'\\\\''")}") 56 | end 57 | end 58 | 59 | # Public: returns the command used to copy a given Item's value to the 60 | # clipboard for the current platform. 61 | # 62 | # Returns a String with the bin 63 | def copy_command 64 | if darwin? 65 | 'pbcopy' 66 | elsif windows? 67 | 'clip' 68 | else 69 | 'xclip -selection clipboard' 70 | end 71 | end 72 | 73 | # Public: copies a given URL value to the clipboard. This method is 74 | # designed to handle multiple platforms. 75 | # 76 | # Returns nothing 77 | def copy(url) 78 | IO.popen(copy_command,"w") {|cc| cc.write(url)} 79 | url 80 | end 81 | 82 | # Public: returns the command used to capture a screenshot 83 | # 84 | def capture_command 85 | if darwin? 86 | 'screencapture' 87 | end 88 | end 89 | 90 | # Public: captures a screenshot and saves to `output` 91 | # 92 | def capture(args, output) 93 | system("#{capture_command} #{args} #{output}") 94 | end 95 | 96 | # Public: opens the JSON file in an editor for you to edit. Uses the 97 | # $EDITOR environment variable, or %EDITOR% on Windows for editing. 98 | # This method is designed to handle multiple platforms. 99 | # If $EDITOR is nil, try to open using the open_command. 100 | # 101 | # Returns a String with a helpful message. 102 | def edit(json_file) 103 | unless $EDITOR.nil? 104 | unless windows? 105 | system("`echo $EDITOR` #{json_file} &") 106 | else 107 | system("start %EDITOR% #{json_file}") 108 | end 109 | else 110 | system("#{open_command} #{json_file}") 111 | end 112 | 113 | 'Make your edits, and do be sure to save.' 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/imgurr/storage.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Storage is the interface between multiple Backends. You can use Storage 3 | # directly without having to worry about which Backend is in use. 4 | # 5 | module Imgurr 6 | class Storage 7 | JSON_FILE = "#{ENV['HOME']}/.imgurr" 8 | 9 | # Public: the path to the Json file used by imgurr. 10 | # 11 | # ENV['IMGURRFILE'] is mostly used for tests 12 | # 13 | # Returns the String path of imgurr's Json representation. 14 | def json_file 15 | ENV['IMGURRFILE'] || JSON_FILE 16 | end 17 | 18 | # Public: initializes a Storage instance by loading in your persisted data from adapter. 19 | # 20 | # Returns the Storage instance. 21 | def initialize 22 | @hashes = Hash.new 23 | bootstrap 24 | populate 25 | end 26 | 27 | # Public: the in-memory collection of all Lists attached to this Storage 28 | # instance. 29 | # 30 | # lists - an Array of individual List items 31 | # 32 | # Returns nothing. 33 | attr_writer :hashes 34 | 35 | # Public: Adds a deletehash to the hashes list 36 | # 37 | # id - Image ID 38 | # hash - Delete hash 39 | # 40 | # Returns nothing 41 | def add_hash(id, hash, source) 42 | @hashes[id] = {:deletehash => hash, :source => source, :stamp => Time.now} 43 | end 44 | 45 | # Public: test whether out storage contains the delete hash for given id 46 | # 47 | # id - ID of the image 48 | # 49 | # Returns true if found, false if not. 50 | def hash_exists?(id) 51 | @hashes.has_key? id 52 | end 53 | 54 | # Public: finds any given delete_hash by id. 55 | # 56 | # name - String name of the list to search for 57 | # 58 | # Returns the first instance of delete_hash that it finds. 59 | def find(id) 60 | hash = @hashes[id] 61 | hash ? hash[:deletehash] : nil 62 | end 63 | 64 | # Public: all Items in storage sorted in chronological order. 65 | # 66 | # Returns an Array of all Items. 67 | def items 68 | @hashes.to_a.sort {|(_, a), (_, b)| a[:stamp] <=> b[:stamp] } 69 | end 70 | 71 | # Public: delete an Item entry from storage. 72 | # 73 | # Returns the deleted Item or nil. 74 | def delete(id) 75 | @hashes.delete(id) 76 | end 77 | 78 | # Public: creates a Hash of the representation of the in-memory data 79 | # structure. This percolates down to Items by calling to_hash on the List, 80 | # which in tern calls to_hash on individual Items. 81 | # 82 | # Returns a Hash of the entire data set. 83 | def to_hash 84 | {:hashes => @hashes} 85 | end 86 | 87 | # Takes care of bootstrapping the Json file, both in terms of creating the 88 | # file and in terms of creating a skeleton Json schema. 89 | # 90 | # Return true if successfully saved. 91 | def bootstrap 92 | return if File.exist?(json_file) 93 | FileUtils.touch json_file 94 | File.open(json_file, 'w') {|f| f.write(to_json) } 95 | save 96 | end 97 | 98 | # Take a JSON representation of data and explode it out into the constituent 99 | # Lists and Items for the given Storage instance. 100 | # 101 | # Returns all hashes. 102 | def populate 103 | file = File.read(json_file) 104 | storage = JSON.parse(file, :symbolize_names => true) 105 | 106 | @hashes = storage[:hashes] 107 | convert if @hashes.is_a? Array 108 | 109 | @hashes 110 | end 111 | 112 | # Public: persists your in-memory objects to disk in Json format. 113 | # 114 | # lists_Json - list in Json format 115 | # 116 | # Returns true if successful, false if unsuccessful. 117 | def save 118 | File.open(json_file, 'w') {|f| f.write(to_json) } 119 | end 120 | 121 | # Public: the Json representation of the current List and Item assortment 122 | # attached to the Storage instance. 123 | # 124 | # Returns a String Json representation of its Lists and their Items. 125 | def to_json 126 | JSON.pretty_generate(to_hash) 127 | end 128 | 129 | private 130 | # Private: convert from old Json representation, filling in the missing data. 131 | # Also print a warning message for the user. 132 | # 133 | # Returns nothing. 134 | def convert 135 | old = @hashes 136 | @hashes = Hash.new 137 | 138 | puts 'Warning: old JSON format detected, converting.' 139 | old.each {|i| add_hash(i[:id], i[:deletehash], 'unknown') } 140 | save 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /test/1-upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export IMGURRFILE=test/files/imgurr.json 3 | imgurr="./bin/imgurr" 4 | image="test/files/habs.png" 5 | id_file="test/id" 6 | 7 | describe "upload" 8 | 9 | it_uploads_image() { 10 | ${imgurr} upload ${image} >> test/temp 11 | sed 's/.*imgur.com\/\(.*\)\..*/\1/' test/temp > ${id_file} 12 | rm test/temp 13 | expr `cat ${id_file} | wc -c` ">" 0 14 | } 15 | 16 | it_uploads_image_with_markdown() { 17 | ${imgurr} upload ${image} --markdown | grep -m 1 "Copied !\[Screenshot\](http://i.imgur.com" 18 | } 19 | 20 | it_uploads_image_with_html() { 21 | ${imgurr} upload ${image} --html | grep -m 1 "Copied \"Screenshot\"" 22 | } 23 | 24 | it_uploads_image_with_html_and_size() { 25 | ${imgurr} upload ${image} --html --size 45 | grep -m 1 "Copied \"Screenshot\"" 26 | } 27 | 28 | it_uploads_image_with_title_desc() { 29 | ${imgurr} upload ${image} --title "Test" --desc "Test" | grep -m 1 "Copied http://i.imgur.com" 30 | } 31 | -------------------------------------------------------------------------------- /test/2-info.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export IMGURRFILE=test/files/imgurr.json 3 | imgurr="./bin/imgurr" 4 | id_file="test/id" 5 | 6 | describe "info" 7 | 8 | it_gets_image_info() { 9 | ${imgurr} info `cat ${id_file}` | grep -m 1 "Width : 48 px" 10 | } 11 | 12 | it_gets_image_info_from_url() { 13 | ${imgurr} info http://i.imgur.com/2KxrTAK.jpg | grep -m 1 "Width : 960 px" 14 | } 15 | 16 | it_gets_image_info_from_url_with_title() { 17 | ${imgurr} info http://i.imgur.com/Wk1iPej.jpg | grep -m 1 "Title : Imgurr Test" 18 | } 19 | 20 | it_gets_image_info_from_url_with_description() { 21 | ${imgurr} info http://i.imgur.com/Wk1iPej.jpg | grep -m 1 "Desc : Imgurr Test" 22 | } 23 | 24 | it_lists_uploaded_images() { 25 | ${imgurr} list | grep -m 1 `cat ${id_file}` 26 | } 27 | -------------------------------------------------------------------------------- /test/3-delete.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export IMGURRFILE=test/files/imgurr.json 3 | imgurr="./bin/imgurr" 4 | id_file="test/id" 5 | 6 | describe "delete" 7 | 8 | it_shows_error_wrong_hash() { 9 | ${imgurr} delete `cat ${id_file}` random | grep -m 1 "Unauthorized Access" 10 | } 11 | 12 | it_deletes_from_storage() { 13 | ${imgurr} delete `cat ${id_file}` | grep -m 1 "Successfully deleted" 14 | } 15 | -------------------------------------------------------------------------------- /test/4-misc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export IMGURRFILE=test/files/imgurr-old.json 3 | imgurr="./bin/imgurr" 4 | 5 | describe "misc" 6 | 7 | it_converts_old_json_file() { 8 | ${imgurr} list | grep -m 1 'Warning' 9 | } 10 | -------------------------------------------------------------------------------- /test/cli.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export IMGURRFILE=test/files/imgurr.json 3 | imgurr="./bin/imgurr" 4 | 5 | describe "cli" 6 | 7 | it_shows_help() { 8 | ${imgurr} --help | grep "imgurr: help" 9 | } 10 | 11 | it_shows_a_version() { 12 | ${imgurr} --version | grep "running imgurr" 13 | } -------------------------------------------------------------------------------- /test/error.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env roundup 2 | export IMGURRFILE=test/files/imgurr.json 3 | imgurr="./bin/imgurr" 4 | image="test/files/habs.gif" 5 | 6 | describe "errors" 7 | 8 | it_shows_missing_arg_error() { 9 | ${imgurr} upload ${image} --title | grep "Error: missing argument" 10 | } 11 | 12 | it_shows_invalid_option_error() { 13 | ${imgurr} upload ${image} --unknown | grep "Error: invalid option" 14 | } -------------------------------------------------------------------------------- /test/files/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chris911/imgurr/09a492a80b9ec1ddf827501e6da6ad3dfa6d224d/test/files/github-logo.png -------------------------------------------------------------------------------- /test/files/habs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chris911/imgurr/09a492a80b9ec1ddf827501e6da6ad3dfa6d224d/test/files/habs.png -------------------------------------------------------------------------------- /test/files/imgurr-old.json: -------------------------------------------------------------------------------- 1 | { 2 | "hashes": [ 3 | { 4 | "id": "77kh0E9", 5 | "deletehash": "zrlumL34gWAg1uv" 6 | }, 7 | { 8 | "id": "aWFr3dt", 9 | "deletehash": "sx5cO3ghdf1xmit" 10 | }, 11 | { 12 | "id": "Xx5SF7N", 13 | "deletehash": "jAZJSrRLvMmx4yw" 14 | }, 15 | { 16 | "id": "qknnpYa", 17 | "deletehash": "luK2SQ6Wzt4KIow" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/roundup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # [r5]: roundup.5.html 3 | # [r1t]: roundup-1-test.sh.html 4 | # [r5t]: roundup-5-test.sh.html 5 | # 6 | # _(c) 2010 Blake Mizerany - MIT License_ 7 | # 8 | # Spray **roundup** on your shells to eliminate weeds and bugs. If your shells 9 | # survive **roundup**'s deathly toxic properties, they are considered 10 | # roundup-ready. 11 | # 12 | # **roundup** reads shell scripts to form test plans. Each 13 | # test plan is sourced into a sandbox where each test is executed. 14 | # 15 | # See [roundup-1-test.sh.html][r1t] or [roundup-5-test.sh.html][r5t] for example 16 | # test plans. 17 | # 18 | # __Install__ 19 | # 20 | # git clone http://github.com/bmizerany/roundup.git 21 | # cd roundup 22 | # make 23 | # sudo make install 24 | # # Alternatively, copy `roundup` wherever you like. 25 | # 26 | # __NOTE__: Because test plans are sourced into roundup, roundup prefixes its 27 | # variable and function names with `roundup_` to avoid name collisions. See 28 | # "Sandbox Test Runs" below for more insight. 29 | 30 | # Usage and Prerequisites 31 | # ----------------------- 32 | 33 | # Exit if any following command exits with a non-zero status. 34 | set -e 35 | 36 | # The current version is set during `make version`. Do not modify this line in 37 | # anyway unless you know what you're doing. 38 | ROUNDUP_VERSION="0.0.5" 39 | export ROUNDUP_VERSION 40 | 41 | # Usage is defined in a specific comment syntax. It is `grep`ed out of this file 42 | # when needed (i.e. The Tomayko Method). See 43 | # [shocco](http://rtomayko.heroku.com/shocco) for more detail. 44 | #/ usage: roundup [--help|-h] [--version|-v] [plan ...] 45 | 46 | roundup_usage() { 47 | grep '^#/' <"$0" | cut -c4- 48 | } 49 | 50 | while test "$#" -gt 0 51 | do 52 | case "$1" in 53 | --help|-h) 54 | roundup_usage 55 | exit 0 56 | ;; 57 | --version|-v) 58 | echo "roundup version $ROUNDUP_VERSION" 59 | exit 0 60 | ;; 61 | --color) 62 | color=always 63 | shift 64 | ;; 65 | -) 66 | echo >&2 "roundup: unknown switch $1" 67 | exit 1 68 | ;; 69 | *) 70 | break 71 | ;; 72 | esac 73 | done 74 | 75 | # Consider all scripts with names matching `*-test.sh` the plans to run unless 76 | # otherwise specified as arguments. 77 | if [ "$#" -gt "0" ] 78 | then 79 | roundup_plans="$@" 80 | else 81 | roundup_plans="$(ls *-test.sh)" 82 | fi 83 | 84 | : ${color:="auto"} 85 | 86 | # Create a temporary storage place for test output to be retrieved for display 87 | # after failing tests. 88 | roundup_tmp="$PWD/.roundup.$$" 89 | mkdir -p "$roundup_tmp" 90 | 91 | trap "rm -rf \"$roundup_tmp\"" EXIT INT 92 | 93 | # __Tracing failures__ 94 | roundup_trace() { 95 | # Delete the first two lines that represent roundups execution of the 96 | # test function. They are useless to the user. 97 | sed '1d' | 98 | # Delete the last line which is the "set +x" of the error trap 99 | sed '$d' | 100 | # Replace the rc=$? of the error trap with an verbose string appended 101 | # to the failing command trace line. 102 | sed '$s/.*rc=/exit code /' | 103 | # Trim the two left most `+` signs. They represent the depth at which 104 | # roundup executed the function. They also, are useless and confusing. 105 | sed 's/^++//' | 106 | # Indent the output by 4 spaces to align under the test name in the 107 | # summary. 108 | sed 's/^/ /' | 109 | # Highlight the last line in front of the exit code to bring notice to 110 | # where the error occurred. 111 | # 112 | # The sed magic puts every line into the hold buffer first, then 113 | # substitutes in the previous hold buffer content, prints that and starts 114 | # with the next cycle. At the end the last line (in the hold buffer) 115 | # is printed without substitution. 116 | sed -n "x;1!{ \$s/\(.*\)/$mag\1$clr/; };1!p;\$x;\$p" 117 | } 118 | 119 | # __Other helpers__ 120 | 121 | # Track the test stats while outputting a real-time report. This takes input on 122 | # **stdin**. Each input line must come in the format of: 123 | # 124 | # # The plan description to be displayed 125 | # d 126 | # 127 | # # A passing test 128 | # p 129 | # 130 | # # A failed test 131 | # f 132 | roundup_summarize() { 133 | set -e 134 | 135 | # __Colors for output__ 136 | 137 | # Use colors if we are writing to a tty device. 138 | if (test -t 1) || (test $color = always) 139 | then 140 | red=$(printf "\033[31m") 141 | grn=$(printf "\033[32m") 142 | mag=$(printf "\033[35m") 143 | clr=$(printf "\033[m") 144 | cols=$(tput cols) 145 | fi 146 | 147 | # Make these available to `roundup_trace`. 148 | export red grn mag clr 149 | 150 | ntests=0 151 | passed=0 152 | failed=0 153 | 154 | : ${cols:=10} 155 | 156 | while read status name 157 | do 158 | case $status in 159 | p) 160 | ntests=$(expr $ntests + 1) 161 | passed=$(expr $passed + 1) 162 | printf " %-48s " "$name:" 163 | printf "$grn[PASS]$clr\n" 164 | ;; 165 | f) 166 | ntests=$(expr $ntests + 1) 167 | failed=$(expr $failed + 1) 168 | printf " %-48s " "$name:" 169 | printf "$red[FAIL]$clr\n" 170 | roundup_trace < "$roundup_tmp/$name" 171 | ;; 172 | d) 173 | printf "%s\n" "$name" 174 | ;; 175 | esac 176 | done 177 | # __Test Summary__ 178 | # 179 | # Display the summary now that all tests are finished. 180 | yes = | head -n 57 | tr -d '\n' 181 | printf "\n" 182 | printf "Tests: %3d | " $ntests 183 | printf "Passed: %3d | " $passed 184 | printf "Failed: %3d" $failed 185 | printf "\n" 186 | 187 | # Exit with an error if any tests failed 188 | test $failed -eq 0 || exit 2 189 | } 190 | 191 | # Sandbox Test Runs 192 | # ----------------- 193 | 194 | # The above checks guarantee we have at least one test. We can now move through 195 | # each specified test plan, determine its test plan, and administer each test 196 | # listed in a isolated sandbox. 197 | for roundup_p in $roundup_plans 198 | do 199 | # Create a sandbox, source the test plan, run the tests, then leave 200 | # without a trace. 201 | ( 202 | # Consider the description to be the `basename` of the plan minus the 203 | # tailing -test.sh. 204 | roundup_desc=$(basename "$roundup_p" -test.sh) 205 | 206 | # Define functions for 207 | # [roundup(5)][r5] 208 | 209 | # A custom description is recommended, but optional. Use `describe` to 210 | # set the description to something more meaningful. 211 | # TODO: reimplement this. 212 | describe() { 213 | roundup_desc="$*" 214 | } 215 | 216 | # Provide default `before` and `after` functions that run only `:`, a 217 | # no-op. They may or may not be redefined by the test plan. 218 | before() { :; } 219 | after() { :; } 220 | 221 | # Seek test methods and aggregate their names, forming a test plan. 222 | # This is done before populating the sandbox with tests to avoid odd 223 | # conflicts. 224 | 225 | # TODO: I want to do this with sed only. Please send a patch if you 226 | # know a cleaner way. 227 | roundup_plan=$( 228 | grep "^it_.*()" $roundup_p | 229 | sed "s/\(it_[a-zA-Z0-9_]*\).*$/\1/g" 230 | ) 231 | 232 | # We have the test plan and are in our sandbox with [roundup(5)][r5] 233 | # defined. Now we source the plan to bring its tests into scope. 234 | . ./$roundup_p 235 | 236 | # Output the description signal 237 | printf "d %s" "$roundup_desc" | tr "\n" " " 238 | printf "\n" 239 | 240 | for roundup_test_name in $roundup_plan 241 | do 242 | # Any number of things are possible in `before`, `after`, and the 243 | # test. Drop into an subshell to contain operations that may throw 244 | # off roundup; such as `cd`. 245 | ( 246 | # Output `before` trace to temporary file. If `before` runs cleanly, 247 | # the trace will be overwritten by the actual test case below. 248 | { 249 | # redirect tracing output of `before` into file. 250 | { 251 | set -x 252 | # If `before` wasn't redefined, then this is `:`. 253 | before 254 | } &>"$roundup_tmp/$roundup_test_name" 255 | # disable tracing again. Its trace output goes to /dev/null. 256 | set +x 257 | } &>/dev/null 258 | 259 | # exit subshell with return code of last failing command. This 260 | # is needed to see the return code 253 on failed assumptions. 261 | # But, only do this if the error handling is activated. 262 | set -E 263 | trap 'rc=$?; set +x; set -o | grep "errexit.*on" >/dev/null && exit $rc' ERR 264 | 265 | # If `before` wasn't redefined, then this is `:`. 266 | before 267 | 268 | # Momentarily turn off auto-fail to give us access to the tests 269 | # exit status in `$?` for capturing. 270 | set +e 271 | ( 272 | # Set `-xe` before the test in the subshell. We want the 273 | # test to fail fast to allow for more accurate output of 274 | # where things went wrong but not in _our_ process because a 275 | # failed test should not immediately fail roundup. Each 276 | # tests trace output is saved in temporary storage. 277 | set -xe 278 | $roundup_test_name 279 | ) >"$roundup_tmp/$roundup_test_name" 2>&1 280 | 281 | # We need to capture the exit status before returning the `set 282 | # -e` mode. Returning with `set -e` before we capture the exit 283 | # status will result in `$?` being set with `set`'s status 284 | # instead. 285 | roundup_result=$? 286 | 287 | # It's safe to return to normal operation. 288 | set -e 289 | 290 | # If `after` wasn't redefined, then this runs `:`. 291 | after 292 | 293 | # This is the final step of a test. Print its pass/fail signal 294 | # and name. 295 | if [ "$roundup_result" -ne 0 ] 296 | then printf "f" 297 | else printf "p" 298 | fi 299 | 300 | printf " $roundup_test_name\n" 301 | ) 302 | done 303 | ) 304 | done | 305 | 306 | # All signals are piped to this for summary. 307 | roundup_summarize -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # A shim to run our tests in shell. 4 | 5 | ./test/roundup test/*.sh 6 | rm test/id 7 | rm test/files/imgurr.json 8 | git checkout -- test/files/imgurr-old.json 9 | 10 | export IMGURRFILE=~/.imgurr 11 | --------------------------------------------------------------------------------