2013-02-19 67 views
5

我喜欢有一个命令行计算器,方便。要求如下:最小打字命令行计算器 - tcsh vs bash

  • 支持所有基本的算术运算符:+, - ,/,*,^为取幂,加括号为分组。
  • 要求最少的打字,我不想调用一个程序与它交互,然后让它退出。
  • 理想情况下,除了表达式本身之外,还应该在命令行中输入一个字符和一个空格。
  • 应该知道如何忽略逗号和美元(或其他货币符号)在数量上 ,让我从网上复制/粘贴,而无需担心 大约有清洁粘贴到计算器之前,每一个数字
  • 要空白宽容的空间存在或缺乏不应导致在保护它从壳表达报价任何错误
  • 没有必要 - 再次最小的打字

的好处,因为tcsh的支持别名位置参数,并且由于别名扩展先于所有其他扩展除了历史扩张之外,在tcsh实现一些接近理想的东西是直接的。

我用这个:

alias C 'echo '\''\!*'\'' |tr -d '\'',\042-\047'\'' |bc -l' 

现在我可以做的东西一样用最少的键入以下:

# the basic stuff: 
tcsh> C 1+2 
3 

# dollar signs, multiplication, exponentiation: 
tcsh> C $8 * 1.07^10 
15.73721085831652257992 

# parentheses, mixed spacing, zero power: 
tcsh> C (2+5)/8 * 2^0 
.87500000000000000000 

# commas in numbers, no problem here either: 
tcsh> C 1,250.21 * 1.5 
1875.315 

正如你可以看到有没有必要引用任何事情,使所有这些工作。

现在出现这个问题。试图做同样在bash,那里参数别名,不支持我的力量来实现计算器作为外壳的功能,并通过使用“$ @”

function C() { echo "[email protected]" | tr -d ', \042-\047' | bc -l; } 

这打破了各种方式的参数,如:

# works: 
bash$ C 1+2 
3 

# works: 
bash$ C 1*2 
2 

# Spaces around '*' lead to file expansion with everything falling apart: 
bash$ C 1 * 2 
(standard_in) 1: syntax error 
(standard_in) 1: illegal character: P 
(standard_in) 1: illegal character: S 
(standard_in) 1: syntax error 
... 

# Non-leading parentheses seem to work: 
bash$ C 2*(2+1) 
6 

# but leading-parentheses don't: 
bash$ C (2+1)*2 
bash: syntax error near unexpected token `2+1' 

当然,在表达式中添加引号可以解决这些问题,但是违背了最初的要求。

我明白为什么东西在打破。我不是在寻找解释。相反,我正在寻找解决方案,它不需要手动引用参数。我对bash向导的疑问是有什么办法可以让bash支持最简单的打字计算器别名。不需要引用,像tcsh一样?这是不可能的吗?谢谢!

+0

您的前两个需求冲突。如果你想要最小的击键次数,那么你想要反向抛光,所以你甚至不需要圆括号来进行分组。 – 2013-02-19 20:21:51

+0

够公平的。我的意思是最小的,同时仍然保留自然的人类符号:) – arielf 2013-02-19 20:51:53

+0

重新考虑这一点:反向抛光将需要两个数字之间的“推/输入”操作符,所以中缀1 + 2(3个字符)实际上需要的输入类型少于反向抛光: 1 2+(4个字符) - 只是一个例子。 – arielf 2013-02-25 05:56:54

回答

2

至少防止*的扩展,可以使用“设置-f”(以下某人的blog post

alias C='set -f -B; Cf ' 
function Cf() { echo "[email protected]" | tr -d ', \042-\047' | bc -l; set +f; }; 

关闭它的别名,在计算之前,然后重新打开之后

$ C 2 * 3 
6 

我下载了bash源文件,看起来非常紧密,似乎括号错误直接发生在命令行解析过程中,在任何命令运行之前o别名被扩展。没有任何标志可以关闭它。 所以不可能从bash脚本执行它。

这意味着,现在是携带重型武器的时候了。在使用readline从标准输入读取命令行解析之前。因此,如果我们拦截对readline的调用,我们可以通过命令行执行任何我们想要的操作。

不幸的是bash与readline是静态链接的,所以这个调用不能被直接拦截。但至少readline是一个全局符号,所以我们可以使用dlsym得到函数的地址,并且通过该地址我们可以在readline中插入任意指令。

修改的readline是直接修剪错误,如果Readline是用不同的bash版本之间的改变,所以我们修改函数调用的ReadLine,导致以下计划:

  1. 找到readline的使用dlsym
  2. 更换的ReadLine用我们自己的函数使用当前栈上的第一个电话找到函数调用的ReadLine(yy_readline_get),然后恢复原来的readline
  3. 修改yy_readline_get调用包装函数
  4. w^ithin包装函数:更换非问题的符号,括号,如果输入与“C”

用C语言编写的AMD64开始,我们得到:

#include <string.h> 
#include <stdio.h> 
#include <stdint.h> 
#include <stdlib.h> 
#ifndef __USE_GNU 
#define __USE_GNU 
#endif 
#ifndef __USE_MISC 
#define __USE_MISC 
#endif 
#include <dlfcn.h> 
#include <unistd.h> 
#include <sys/mman.h> 
#include <errno.h> 

//-----------Assembler helpers---------- 

#if (defined(x86_64) || defined(__x86_64__)) 

    //assembler instructions to read rdp, which we need to read the stack 
#define MOV_EBP_OUT "mov %%rbp, %0" 
    //size of a call instruction 
#define RELATIVE_CALL_INSTRUCTION_SIZE 5 

#define IS64BIT (1) 

    /* 
     To replace a function with a new one, we use the push-ret trick, pushing the destination address on the stack and let ret jump "back" to it 
     This has the advantage that we can set an additional return address in the same way, if the jump goes to a function 

    This struct corresponds to the following assembler fragment:   
    68  ???? push     <low_dword (address)> 
    C7442404 ???? mov DWORD PTR [rsp+4], <high_dword (address)) 
    C3    ret 
    */ 
typedef struct __attribute__((__packed__)) LongJump { 
    char push; unsigned int destinationLow; 
    unsigned int mov_dword_ptr_rsp4; unsigned int destinationHigh; 
    char ret; 
// char nopFiller[16]; 
} LongJump; 

void makeLongJump(void* destination, LongJump* res) { 
    res->push = 0x68; 
    res->destinationLow = (uintptr_t)destination & 0xFFFFFFFF; 
    res->mov_dword_ptr_rsp4 = 0x042444C7; 
    res->destinationHigh = ((uintptr_t)(destination) >> 32) & 0xFFFFFFFF; 
    res->ret = 0xC3; 
} 

//Macros to save and restore the rdi register, which is used to pass an address to readline (standard amd64 calling convention) 
typedef unsigned long SavedParameter; 
#define SAVE_PARAMETERS SavedParameter savedParameters; __asm__("mov %%rdi, %0": "=r"(savedParameters)); 
#define RESTORE_PARAMETERS __asm__("mov %0, %%rdi": : "r"(savedParameters)); 

#else 
#error only implmented for amd64... 
#endif 

//Simulates the effect of the POP instructions, popping from a passed "stack pointer" and returning the popped value 
static void * pop(void** stack){ 
    void* temp = *(void**)(*stack); 
    *stack += sizeof(void*); 
    return temp; 
} 

//Disables the write protection of an address, so we can override it 
static int unprotect(void * POINTER){ 
    const int PAGESIZE = sysconf(_SC_PAGE_SIZE);; 
    if (mprotect((void*)(((uintptr_t)POINTER & ~(PAGESIZE-1))), PAGESIZE, PROT_READ | PROT_WRITE | PROT_EXEC)) { 
    fprintf(stderr, "Failed to set permission on %p\n", POINTER); 
    return 1; 
    } 
    return 0; 
} 

//Debug stuff 
static void fprintfhex(FILE* f, void * hash, int len) { 
    for (int i=0;i<len;i++) { 
    if ((uintptr_t)hash % 8 == 0 && (uintptr_t)i % 8 == 0 && i) fprintf(f, " "); 
    fprintf(f, "%.2x", ((unsigned char*)(hash))[i]); 
    } 
    fprintf(f, "\n"); 
} 

//--------------------------------------- 


//Address of the original readline function 
static char* (*real_readline)(const char*)=0; 

//The wrapper around readline we want to inject. 
//It replaces() with [], if the command line starts with "C " 
static char* readline_wrapper(const char* prompt){ 
    if (!real_readline) return 0; 
    char* result = real_readline(prompt); 
    char* temp = result; while (*temp == ' ') temp++; 
    if (temp[0] == 'C' && temp[1] == ' ') 
    for (int len = strlen(temp), i=0;i<len;i++) 
     if (temp[i] == '(') temp[i] = '['; 
     else if (temp[i] == ')') temp[i] = ']'; 
    return result; 
} 


//Backup of the changed readline part 
static unsigned char oldreadline[2*sizeof(LongJump)] = {0x90}; 
//A wrapper around the readline wrapper, needed on amd64 (see below) 
static LongJump* readline_wrapper_wrapper = 0; 



static void readline_initwrapper(){ 
    SAVE_PARAMETERS 
    if (readline_wrapper_wrapper) { fprintf(stderr, "ERROR!\n"); return; } 

    //restore readline 
    memcpy(real_readline, oldreadline, 2*sizeof(LongJump)); 

    //find call in yy_readline_get 
    void * frame; 
    __asm__(MOV_EBP_OUT: "=r"(frame)); //current stackframe 
    pop(&frame); //pop current stackframe (??) 
    void * returnToFrame = frame; 
    if (pop(&frame) != real_readline) { 
    //now points to current return address 
    fprintf(stderr, "Got %p instead of %p=readline, when searching caller\n", frame, real_readline); 
    return; 
    } 
    void * caller = pop(&frame); //now points to the instruction following the call to readline 
    caller -= RELATIVE_CALL_INSTRUCTION_SIZE; //now points to the call instruction 
    //fprintf(stderr, "CALLER: %p\n", caller); 
    //caller should point to 0x00000000004229e1 <+145>: e8 4a e3 06 00 call 0x490d30 <readline> 
    if (*(unsigned char*)caller != 0xE8) { fprintf(stderr, "Expected CALL, got: "); fprintfhex(stderr, caller, 16); return; } 

    if (unprotect(caller)) return; 

    //We can now override caller to call an arbitrary function instead of readline. 
    //However, the CALL instruction accepts only a 32 parameter, so the called function has to be in the same 32-bit address space 
    //Solution: Allocate memory at an address close to that CALL instruction and put a long jump to our real function there 
    void * hint = caller; 
    readline_wrapper_wrapper = 0; 
    do { 
    if (readline_wrapper_wrapper) munmap(readline_wrapper_wrapper, 2*sizeof(LongJump)); 
    readline_wrapper_wrapper = mmap(hint, 2*sizeof(LongJump), PROT_EXEC | PROT_READ | PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); 
    if (readline_wrapper_wrapper == MAP_FAILED) { fprintf(stderr, "mmap failed: %i\n", errno); return; } 
    hint += 0x100000; 
    } while (IS64BIT && ((uintptr_t)readline_wrapper_wrapper >= 0xFFFFFFFF + ((uintptr_t) caller))); //repeat until we get an address really close to caller 
    //fprintf(stderr, "X:%p\n", readline_wrapper_wrapper); 
    makeLongJump(readline_wrapper, readline_wrapper_wrapper); //Write the long jump in the newly allocated space 

    //fprintfhex(stderr, readline_wrapper_wrapper, 16); 
    //fprintfhex(stderr, caller, 16); 

    //patch caller to become call <readline_wrapper_wrapper> 
    //called address is relative to address of CALL instruction 
    *(uint32_t*)(caller+1) = (uint32_t) ((uintptr_t)readline_wrapper_wrapper - (uintptr_t)(caller + RELATIVE_CALL_INSTRUCTION_SIZE)); 

    //fprintfhex(stderr, caller, 16); 

    *(void**)(returnToFrame) = readline_wrapper_wrapper; //change stack to jump to wrapper instead real_readline (or it would not work on the first entered command) 

    RESTORE_PARAMETERS 
} 




static void _calc_init(void) __attribute__ ((constructor)); 


static void _calc_init(void){ 
    if (!real_readline) { 
    //Find readline 
    real_readline = (char* (*)(const char*)) dlsym(RTLD_DEFAULT, "readline"); 
    if (!real_readline) return; 
    //fprintf(stdout, "loaded %p\n", real_readline); 
    //fprintf(stdout, " => %x\n", * ((int*) real_readline)); 

    if (unprotect(real_readline)) { fprintf(stderr, "Failed to unprotect readline\n"); return; } 
    memcpy(oldreadline, real_readline, 2*sizeof(LongJump)); //backup readline's instructions 

    //Replace readline with readline_initwrapper 
    makeLongJump(real_readline, (LongJump*)real_readline); //add a push/ret long jump from readline to readline, to have readline's address on the stack in readline_initwrapper 
    makeLongJump(readline_initwrapper, (LongJump*)((char*)real_readline + sizeof(LongJump) - 1)); //add a push/ret long jump from readline to readline_initwrapper, overriding the previous RET 

    } 
} 

这可以被编译成拦截与库:

gcc -g -std=c99 -shared -fPIC -o calc.so -ldl calc.c 

,然后装在bash有:

gdb --batch-silent -ex "attach $BASHPID" -ex 'print dlopen("calc.so", 0x101)' 

现在,当先前的别名用括号更换扩展装载:

alias C='set -f -B; Cf ' 
function Cf() { echo "[email protected]" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | bc -l; set +f; }; 

我们可以这样写:

$ C 1 * 2 
    2 
$ C 2*(2+1) 
    6 
$ C (2+1)*2 
    6 

更妙的是它成为,如果我们从公元前切换到qalculate

alias C='set -f -B; Cf ' 
function Cf() { echo "[email protected]" | tr -d ', \042-\047' | tr [ '(' | tr ] ')' | xargs qalc ; set +f; }; 

然后我们可以这样做:

$ C e^(i * pi) 
    e^(i * pi) = -1 

$ C 3 c 
    3 * speed_of_light = approx. 899.37737(km/ms) 
+0

很酷。感谢您的提示!这对我最初的解决方案是一个很大的改进。感谢帮助。 – arielf 2013-02-19 20:52:40

+0

另一个怪癖:这个修改打破了其他表达式:'C 1 * 2'给出了0,'C 1 + 2'给出了2. – arielf 2013-02-19 21:06:38

+0

奇怪,它对我来说工作得很好。 – BeniBela 2013-02-19 21:14:21

3

如果你准备键入C输入的代替C空间,天空才是极限。命令C可以采用任何您想要的形式输入,与shell语法无关。

C() { 
    local line 
    read -p "Arithmetic: " -e line 
    echo "$line" | tr -d \"-\', | bc -l 
} 

在zsh中:

function C { 
    local line= 
    vared -p "Arithmetic: " line 
    echo $line | tr -d \"-\', | bc -l 
} 

在zsh中,您可以关闭通配符用于与noglob modifier特定命令的参数。它通常隐藏在别名中。这可以防止*^()从字面上开始解释,但不会引用或$

quickie_arithmetic() { 
    echo "$*" | tr -d \"-\', | bc -l 
} 
alias C='noglob quickie_arithmetic' 
+0

谢谢!这是一个非常聪明的转折。事后看来很明显,但当我提出原始问题时,我完全错过了它的可能性。 – arielf 2013-04-10 01:21:37